- Status Closed
-
Assigned To
cbay - Private
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
Loading...
Available keyboard shortcuts
- Alt + ⇧ Shift + l Login Dialog / Logout
- Alt + ⇧ Shift + a Add new task
- Alt + ⇧ Shift + m My searches
- Alt + ⇧ Shift + t focus taskid search
Tasklist
- o open selected task
- j move cursor down
- k move cursor up
Task Details
- n Next task
- p Previous task
- Alt + ⇧ Shift + e ↵ Enter Edit this task
- Alt + ⇧ Shift + w watch task
- Alt + ⇧ Shift + y Close Task
Task Editing
- Alt + ⇧ Shift + s save task
Hello,
Not having "Cache-Control" headers on a page (500 or not) is not an issue.
Kind regards,
Cyril