Skip to content

The Apache path traversal (CVE-2021-41773)

Severity: criticalApplies to: Apache httpd 2.4.49 / 2.4.50 (the vulnerable ones)Applies to: Any 2.4.x with a loosened root Directory

In October 2021, Apache 2.4.49 shipped with a change to how it normalised request paths. Within days, servers running it were having /etc/passwd read over the web, and — where mod_cgi was loaded — having commands run on them. It was exploited in the wild within hours of disclosure, by unauthenticated attackers, with a one-line curl.

It is the clearest worked example on this site of two things: how thin the line is, and how much Apache’s defaults were doing to hold it.

Apache is supposed to reject .. sequences that would climb above the document root. CVE-2021-41773 was a flaw in that check: a .. encoded as %2e%2e slipped through normalisation, so a request like

GET /cgi-bin/.%2e/.%2e/.%2e/.%2e/etc/passwd

resolved, on the filesystem, to /etc/passwd.

The 2.4.50 fix was incomplete — it didn’t account for double encoding — so CVE-2021-42013 followed days later with %%32%65 in place of .. Two CVEs, one bug, one week.

With mod_cgi loaded, the same traversal reached executables and ran them, turning file disclosure into remote code execution:

POST /cgi-bin/.%2e/.%2e/bin/sh (with a command in the body)

This is the part worth internalising, because it’s the lesson rather than the trivia.

The exploit only reached files Apache was willing to serve. A default Apache config carries this:

<Directory />
Require all denied
</Directory>

That denies the entire filesystem by default and grants access back only for specific document roots. So even with the traversal bug, a request for /etc/passwd hit Require all denied and got a 403 — the bug climbed out of the document root and immediately ran into a wall.

The servers that got hit were the ones that had replaced that default with Require all granted — usually to make some unrelated 403 go away. They’d removed the wall, so when the traversal bug arrived, there was nothing behind it.

That’s why keeping Require all denied is control #10 on this site. It is not defence in depth against a hypothetical — it is the specific thing that decided who got compromised in a real, widely-exploited event.

  1. Patch. 2.4.49 and 2.4.50 are the vulnerable versions; 2.4.51 fixed both, and you should be far past that — 2.4.68 is current. If you’re on 2.4.49 or 2.4.50, this is not a hardening task, it’s an incident.
  2. Keep Require all denied on <Directory /> — the control that contained it even unpatched.
  3. Disable mod_cgi/mod_cgid — the difference between file disclosure and code execution. If you serve PHP via FPM or proxy to an app, you don’t need CGI.

Steps 2 and 3 are the durable ones: they mean the next path bug — and there will be one — is contained the way this one was for the servers that kept their defaults.