Skip to content

fix: block TRACE/TRACK methods - #1172

Open
marlonkeating wants to merge 2 commits into
masterfrom
trace-ddos-fix
Open

fix: block TRACE/TRACK methods#1172
marlonkeating wants to merge 2 commits into
masterfrom
trace-ddos-fix

Conversation

@marlonkeating

Copy link
Copy Markdown
Contributor

Summary

democracylab.org experienced a volumetric DDoS attack using HTTP TRACE requests with randomised cache-busting query parameters. The attack saturated Heroku's dyno request backlog, causing H11 errors and 503 responses for legitimate users.

Code fix

File: common/helpers/malicious_requests.py

  • Added _ALWAYS_BLOCKED_METHODS = frozenset({'TRACE', 'TRACK'}) — blocked unconditionally, no env var needed.
  • check_request_method() is now the first check in __call__, short-circuiting before URL/IP checks.
  • Removed the MiddlewareNotUsed raise: the middleware is always active for method blocking even when neither pattern env var is set.
Details

Sample log line

Jul 10 01:30:07 PM democracy-lab heroku/router at=error code=H11 desc="Backlog too deep"
method=TRACE path="/user/25463?page=1&Lt4ZGViwIr=RiPKDIuy8N"
host=www.democracylab.org fwd="" status=503

Impact

  • Heroku H11 ("Backlog too deep") errors returned as 503 to real users
  • No data breach or data loss
  • No authentication bypass

Attack mechanics

Signal Meaning
method=TRACE TRACE is never used by browsers or legitimate API clients. Cheap to send, forces the server to echo the request body back — used here as a flood vector. Also enables Cross-Site Tracing (XST) if reflected.
Lt4ZGViwIr=RiPKDIuy8N (randomised query param) Each request has a unique key=value pair (~10 random chars each). Defeats any upstream HTTP cache (Heroku router, CDN) so every request reaches a dyno.
fwd="" (empty X-Forwarded-For) Heroku's router populates X-Forwarded-For for all inbound traffic. An empty value suggests the attacker reached the app through an atypical path, or stripped the header. Makes IP-based blocking unreliable for this incident.

Background: TRACE and TRACK

TRACE is defined in RFC 9110 §9.3.8 (the current HTTP semantics standard). It is a diagnostic method — the server echoes back the received request so the client can inspect what intermediaries modified in transit. No browser or legitimate API client sends TRACE in production; its only real-world use is manual debugging with curl or similar tools.

Cross-Site Tracing (XST): Described by Jeremiah Grossman in 2003 and documented in OWASP's XST article. Because TRACE echoes all request headers — including HttpOnly cookies and Authorization headers — back in the response body, a malicious script can read that body and exfiltrate credentials that HttpOnly was specifically designed to keep out of JS reach. Modern browsers now block cross-origin TRACE requests, but the method remains a defense-in-depth concern.

TRACK is a Microsoft IIS-specific, non-standard variant of TRACE — not defined in any RFC. It carries the same XST risks with no legitimate use case on a Django application.

OWASP's HTTP Methods testing guide recommends disabling both methods on all production servers.

In this incident TRACE was not used for XST but as a flood vector: it is cheap to send, forces the server to process and echo the request body, and — combined with cache-busting query params — bypasses all upstream caching so every request reaches a dyno.

Why existing defences didn't catch it

  1. DRF throttling (AnonRateThrottle, UserRateThrottle) only wraps DRF views. /user/<id> is a standard Django view, so throttling never fired.
  2. MaliciousRequestsMiddleware filtered by URL pattern and forwarded-for IP, but had no HTTP method check. It also raised MiddlewareNotUsed when neither env var was set, removing itself from the stack entirely.
  3. No edge-layer protection — all traffic reaches Heroku dynos; there is no CDN or WAF in front.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the Django MaliciousRequestsMiddleware to mitigate a TRACE/TRACK-based volumetric DDoS by blocking those HTTP methods unconditionally and ensuring method blocking happens before URL/IP checks.

Changes:

  • Added unconditional blocking for TRACE and TRACK via _ALWAYS_BLOCKED_METHODS and a new check_request_method() guard.
  • Adjusted middleware execution flow so method blocking short-circuits before URL/FWD pattern checks, and removed MiddlewareNotUsed so the middleware remains active.
  • Added a new test module covering always-blocked methods plus existing URL/FWD pattern behavior.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
common/helpers/malicious_requests.py Adds unconditional TRACE/TRACK blocking and keeps middleware active even without env-configured patterns.
common/tests/test_malicious_requests_middleware.py Adds tests for method blocking and existing URL/FWD filtering behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 13 to 18
if settings.MALICIOUS_URL_PATTERNS is not None:
url_patterns = settings.MALICIOUS_URL_PATTERNS.split(',')
self.malicious_url_patterns = list(map(lambda pattern: re.compile(pattern, re.IGNORECASE), url_patterns))
used = True
if settings.MALICIOUS_FWD_PATTERNS is not None:
fwd_patterns = settings.MALICIOUS_FWD_PATTERNS.split(',')
self.malicious_fwd_patterns = list(map(lambda pattern: re.compile(pattern, re.IGNORECASE), fwd_patterns))
Comment thread common/helpers/malicious_requests.py Outdated
Comment thread common/helpers/malicious_requests.py Outdated
Comment on lines 45 to 46
hasattr(self, 'malicious_url_patterns') and self.check_request_url(request)
hasattr(self, 'malicious_fwd_patterns') and self.check_request_fwd(request)
Comment on lines +99 to +105
def test_clean_fwd_is_allowed(self):
mw = _middleware()
mw(_make_request(fwd='203.0.113.5'))

def test_remote_addr_used_when_no_fwd_header(self):
mw = _middleware()
mw(_make_request()) # REMOTE_ADDR is 127.0.0.1, should not match ^10\.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants