Security vulnerabilities

  • Status Closed
  • Assigned To
    cbay
  • Private
Attached to Project: Security vulnerabilities
Opened by subhash - 12.07.2026
Last edited by cbay - 13.07.2026

FS#396 - Server Crash via X-Forwarded-Host

While testing the admin panel, I noticed that sending any request with an `@` character in the `X-Forwarded-Host` header causes a 500 Internal Server Error:

```
GET / HTTP/1.1
Host: admin.alwaysdata.com
X-Forwarded-Host: admin.alwaysdata.com@evil.com ```

Response: 500 Internal Server Error

This is reproducible on every endpoint (`/`, `/login/`, `/password/lost/`, `/support/`). Any form of `@` in the XFH triggers it — `evil@admin.alwaysdata.com`, `@evil.com`, `admin.alwaysdata.com:443@evil.com` all work.

The 500 error page does not include `Cache-Control` headers, while normal responses include `Cache-Control: max-age=0, no-cache, no-store, must-revalidate, private`. If there is any caching layer between the client and the Django application (Varnish, CDN, nginx proxy_cache), this could be turned into a cache-poisoning denial-of-service — an attacker sends the poisoned request, the 500 gets cached, and all subsequent users see the error page.

The likely cause is Django's `get_host()` method (with `USE_X_FORWARDED_HOST = True`) choking on the `@` character during `ALLOWED_HOSTS` validation. This should raise a `SuspiciousOperation`/`DisallowedHost` and return a 400, not a 500.

### Severity

Low. Denial of service, no data exposure. But the missing cache-control headers on the error response are worth fixing regardless.

## Remediation Steps

1. Validate `vhost_additional_directives` — Implement an allowlist of permitted Apache directives. At minimum, block: `Alias`, `ProxyPass`, `ProxyPassReverse`, `Options`, `AddHandler`, `AddOutputFilter`, `Include`, `Action`, `Script`, `SetHandler`, `<Directory>`, `<Location>`, and any directive that can read files, proxy connections, or execute code.

2. Fix /tmp isolation — Ensure each tenant's processes use a private `/tmp` (e.g., via `PrivateTmp=yes` in systemd, or mount namespaces). The  FS#363  fix should be re-evaluated.

3. Handle `@` in X-Forwarded-Host — Add input validation for the XFH header before it reaches Django's `get_host()`. Return 400 for malformed hosts. Add `Cache-Control: no-store` to all error responses.

## Steps to Reproduce

### Step 1 — Send request with @ in X-Forwarded-Host

```http
GET /login/ HTTP/1.1
Host: admin.alwaysdata.com
X-Forwarded-Host: admin.alwaysdata.com@evil.com ```

Response:

```http
HTTP/1.1 500 Internal Server Error
Server: nginx
Content-Type: text/html
```

No `Cache-Control`, no `Content-Security-Policy`, no `X-Content-Type-Options`, no `Referrer-Policy`.

### Step 2 — Compare with normal response headers

Normal response:

```http
HTTP/1.1 200 OK
Cache-Control: max-age=0, no-cache, no-store, must-revalidate, private
Content-Security-Policy: base-uri 'self'; frame-ancestors 'self'
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
```

Error response: All security headers missing.

### Step 3 — Verify all @ positions trigger the crash

X-Forwarded-Host value Result
———————— ——–
`admin.alwaysdata.com@evil.com` 500
`evil@admin.alwaysdata.com` 500
`@evil.com` 500
`admin@evil` 500
Every variant containing `@` triggers the crash.

## Root Cause

Django's `HttpRequest.get_host()` processes the `X-Forwarded-Host` header (because `USE_X_FORWARDED_HOST = True`). The `@` character is interpreted as a URL userinfo separator, causing the URL parsing to fail with an unhandled exception instead of triggering the `DisallowedHost` handler (which returns a clean `400 Bad Request`).

Thanks

Closed by  cbay
13.07.2026 12:07
Reason for closing:  Invalid
Admin
cbay commented on 13.07.2026 12:07

Hello,

Not having "Cache-Control" headers on a page (500 or not) is not an issue.

Kind regards,
Cyril

Loading...

Available keyboard shortcuts

Tasklist

Task Details

Task Editing