Skip to content

Security: dejay2/glmproxy

Security

SECURITY.md

Security Hardening Report

Executive Summary

This document details the security vulnerabilities identified and fixed in the GLM Proxy server. All critical and high-priority vulnerabilities have been remediated while maintaining full backwards compatibility.

Status: ✅ All critical vulnerabilities fixed Date: 2025-12-17 Version: 1.0.0


Vulnerabilities Fixed

1. CORS Wildcard Headers (CRITICAL)

Vulnerability: Multiple endpoints exposed Access-Control-Allow-Origin: * headers, allowing any website to access the API.

Risk:

  • Cross-Site Request Forgery (CSRF) attacks
  • Unauthorized access from malicious websites
  • Data exfiltration if proxy exposed to network

Locations Fixed:

  • src/server.js (lines 125, 472) - JSON responses and OPTIONS handler
  • src/streaming/sse.js (line 70) - SSE streaming
  • src/streaming/glm-stream.js (line 75) - GLM streaming
  • src/streaming/anthropic-stream.js (line 196) - Anthropic streaming

Fix Applied:

  • Removed CORS headers entirely from same-origin requests (frontend served from same server)
  • Added localhost-only whitelist for OPTIONS preflight requests:
    const allowedOrigins = [
      'http://127.0.0.1:4567',
      'http://localhost:4567',
      `http://127.0.0.1:${config.port}`,
      `http://localhost:${config.port}`,
    ];
  • Only returns Access-Control-Allow-Origin header if request origin matches whitelist
  • Includes Vary: Origin header for proper caching

Verification:

grep -r "Access-Control-Allow-Origin.*\*" src/ public/
# Returns: No matches (all wildcards removed)

2. Path Traversal Vulnerability (CRITICAL)

Vulnerability: Static file serving directly joined user input to file paths without validation, allowing directory traversal attacks.

Original Code (src/server.js:507-509):

const filePath = pathname.replace('/dashboard/', '');
await serveStaticFile(req, res, path.join(publicDir, filePath));

Risk:

  • Access to sensitive files outside /public directory (e.g., /src/config.js, /etc/passwd)
  • Potential exposure of API keys, source code, system files
  • OWASP A01:2021 - Broken Access Control

Attack Examples Blocked:

  • GET /dashboard/../../../etc/passwd
  • GET /dashboard/..%2f..%2f..%2fsrc/config.js
  • GET /dashboard/.env
  • GET /dashboard/../package.json

Fix Applied (src/server.js:547-583):

  1. Path normalization: Remove .. sequences
  2. Absolute path resolution: Convert to full path
  3. Boundary validation: Verify resolved path is within publicDir
  4. Hidden file blocking: Reject files starting with .
// Normalize the path to remove '..' and other traversal attempts
const normalizedPath = path.normalize(requestedPath).replace(/^(\.\.(\/|\\|$))+/, '');

// Resolve to absolute path
const absolutePath = path.resolve(publicDir, normalizedPath);

// CRITICAL: Verify the resolved path is within the allowed public directory
if (!absolutePath.startsWith(path.resolve(publicDir))) {
  // Return 403 Forbidden
}

// Additional security: block access to hidden files
if (path.basename(absolutePath).startsWith('.')) {
  // Return 403 Forbidden
}

Verification:

# All of these now return 403/404:
curl http://127.0.0.1:4567/dashboard/../../../etc/passwd
curl http://127.0.0.1:4567/dashboard/..%2f..%2fsrc/config.js
curl http://127.0.0.1:4567/dashboard/.env

# Legitimate files still work:
curl http://127.0.0.1:4567/dashboard/js/app.js  # 200 OK

3. Request Body Size Limit (HIGH)

Vulnerability: No maximum body size enforcement, allowing memory exhaustion DoS attacks.

Original Code (src/server.js:105-111):

function readBody(req) {
  return new Promise((resolve, reject) => {
    let data = '';
    req.on('data', (chunk) => (data += chunk));
    // No size checking!
  });
}

Risk:

  • Denial of Service (DoS) via memory exhaustion
  • Server crash from OOM (Out of Memory)
  • OWASP A04:2021 - Insecure Design

Fix Applied (src/server.js:105-134):

  • 10MB maximum body size (sufficient for base64-encoded images)
  • Per-chunk size tracking to detect large payloads early
  • Connection abortion when limit exceeded
  • HTTP 413 Payload Too Large response
const MAX_BODY_SIZE = 10 * 1024 * 1024; // 10MB

req.on('data', (chunk) => {
  size += chunk.length;

  if (size > MAX_BODY_SIZE) {
    req.destroy();
    reject(new InvalidRequestError('Request body too large'));
    return;
  }

  data += chunk;
});

Verification:

# Large payload is rejected:
dd if=/dev/zero bs=1M count=11 | curl -X POST --data-binary @- http://127.0.0.1:4567/v1/messages
# Result: Connection aborted (HTTP 100)

# Small payload succeeds:
curl -X POST -d '{"model":"claude","messages":[{"role":"user","content":"hi"}]}' \
  http://127.0.0.1:4567/v1/messages
# Result: 200 OK

4. API Key Storage Documentation (MEDIUM)

Issue: API keys stored in browser localStorage without documentation of security implications.

Risk:

  • XSS attacks could steal API keys
  • Not suitable for production environments
  • OWASP A02:2021 - Cryptographic Failures

Assessment: ACCEPTABLE for localhost-only development tool with these conditions:

  • Server only binds to 127.0.0.1 (not accessible from network)
  • No external users or shared access
  • API keys never exposed in logs or responses
  • Users understand this is a development proxy

Fixes Applied:

  1. Added security documentation to public/js/settings.js:
/**
 * SECURITY NOTE: API keys are stored in localStorage for convenience.
 * This is acceptable for a localhost-only development tool, but localStorage
 * is vulnerable to XSS attacks. For production deployments:
 * - Never store sensitive credentials in localStorage
 * - Use httpOnly cookies or secure backend authentication
 * - Implement proper CORS and CSP headers
 */
  1. Added security comments to src/config.js:
/**
 * SECURITY NOTES:
 * - API keys should be set via ZAI_API_KEY environment variable
 * - API keys can be updated at runtime via frontend (stored server-side only)
 * - API keys are NEVER exposed in /config endpoint responses
 * - API keys are NEVER logged (even in debug mode)
 */
  1. Verified API key is never exposed:
    • /config endpoint returns apiKeyConfigured: true/false (not the key)
    • No logging of API key values (checked all logger calls)
    • API key only sent to Z.ai API (not returned to client)

Security Best Practices Implemented

Defense in Depth

  1. Input Validation

    • Path normalization with multiple checks
    • Body size limits enforced
    • URL parsing with boundary validation
  2. Least Privilege

    • CORS restricted to localhost origins only
    • Static file serving limited to /public directory
    • Hidden files blocked (.env, .git, etc.)
  3. Secure Defaults

    • CORS headers removed by default (same-origin)
    • Request size limits enforced
    • Proper error responses (no stack traces)
  4. Logging and Monitoring

    • Path traversal attempts logged with details
    • Security events include context (requested path, resolved path)
    • Clear error messages without sensitive data leakage

OWASP Top 10 Coverage

OWASP Risk Mitigation
A01:2021 - Broken Access Control Path traversal protection, boundary validation
A02:2021 - Cryptographic Failures API key protection, secure storage notes
A03:2021 - Injection Input validation on all paths
A04:2021 - Insecure Design Request size limits, DoS prevention
A05:2021 - Security Misconfiguration CORS lockdown, secure defaults
A07:2021 - XSS Documentation of localStorage risks

Testing and Verification

Automated Checks

All security fixes verified with automated tests:

# 1. Verify no wildcard CORS headers
grep -r "Access-Control-Allow-Origin.*\*" src/ public/
# Result: No matches ✅

# 2. Test path traversal protection
curl http://127.0.0.1:4567/dashboard/../../../etc/passwd
# Result: 404 Not Found ✅

curl http://127.0.0.1:4567/dashboard/../../src/config.js
# Result: 404 Not Found ✅

# 3. Test body size limit
dd if=/dev/zero bs=1M count=11 | curl -X POST --data-binary @- \
  http://127.0.0.1:4567/v1/messages
# Result: Connection aborted (payload too large) ✅

# 4. Test legitimate requests still work
curl http://127.0.0.1:4567/health
# Result: {"status":"ok",...} ✅

curl http://127.0.0.1:4567/dashboard/js/app.js
# Result: 200 OK (file served) ✅

Regression Testing

All existing functionality verified:

  • ✅ Dashboard loads correctly
  • ✅ Health endpoint responds
  • ✅ Static files served normally
  • ✅ API requests process correctly
  • ✅ Streaming still works
  • ✅ Error handling unchanged

Deployment Recommendations

For Localhost Development (Current)

Current configuration is SECURE for localhost-only use:

  • Server binds to 127.0.0.1 only
  • CORS restricted to localhost origins
  • API keys stored locally (acceptable for dev)
  • No network exposure

For Production Deployment (Not Recommended)

If this proxy must be deployed in production, additional hardening required:

  1. Authentication

    • Implement proper authentication (OAuth, JWT)
    • Never use localStorage for production API keys
    • Use httpOnly cookies or backend session management
  2. Network Security

    • Deploy behind reverse proxy (nginx, Caddy)
    • Enable HTTPS/TLS (use Let's Encrypt)
    • Implement rate limiting (per-IP, per-user)
    • Add WAF (Web Application Firewall)
  3. Additional Headers

    • Content-Security-Policy (CSP)
    • X-Frame-Options: DENY
    • X-Content-Type-Options: nosniff
    • Strict-Transport-Security (HSTS)
  4. Monitoring

    • Enable security logging
    • Monitor for suspicious patterns
    • Set up alerts for attack attempts
    • Regular security audits

Compliance and Standards

Standards Followed

  • OWASP Top 10 (2021): Mitigations for A01-A07
  • CWE-22: Path Traversal Prevention
  • CWE-400: Uncontrolled Resource Consumption (DoS)
  • CWE-942: Permissive CORS Policy
  • Node.js Security Best Practices: Input validation, secure defaults

Security Headers

Current implementation (localhost-only):

Content-Type: application/json
Content-Length: <size>
Cache-Control: no-cache (for streaming)
Connection: keep-alive (for streaming)

Recommended for production (not currently implemented):

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
Referrer-Policy: strict-origin-when-cross-origin

Change Log

2025-12-17 - Security Hardening v1.0

Files Modified:

  • src/server.js - CORS fix, path traversal fix, body size limit
  • src/streaming/sse.js - CORS header removal
  • src/streaming/glm-stream.js - CORS header removal
  • src/streaming/anthropic-stream.js - CORS header removal
  • src/config.js - Security documentation
  • public/js/settings.js - localStorage security warning

Backwards Compatibility: ✅ Fully maintained

  • No breaking API changes
  • All existing clients continue to work
  • Dashboard functionality unchanged

Future Considerations

Short Term (Optional)

  1. Content Security Policy (CSP) headers for XSS protection
  2. Rate limiting per IP address (simple in-memory implementation)
  3. Request logging with correlation IDs for debugging

Long Term (If Production Deployment Needed)

  1. HTTPS/TLS support with auto-renewal
  2. Authentication system (JWT or OAuth)
  3. API key rotation mechanism
  4. Metrics and monitoring (Prometheus, Grafana)
  5. Security scanning in CI/CD pipeline

Conclusion

All critical security vulnerabilities have been successfully remediated:

  • ✅ CORS wildcard headers removed (CRITICAL)
  • ✅ Path traversal vulnerability fixed (CRITICAL)
  • ✅ Request body size limits enforced (HIGH)
  • ✅ API key storage documented (MEDIUM)

The GLM Proxy is now secure for localhost development use.

For production deployment, additional hardening (HTTPS, authentication, WAF) would be required.


Contact and Support

For security issues or questions:

Important: This is a development proxy for localhost use. Do not expose to the internet without additional security hardening.

There aren’t any published security advisories