In a path, /./ means "this directory". It changes nothing about where the path points, which is exactly why it is useful to an attacker. CWE-55 is path equivalence using the single dot directory sequence. The product accepts path input containing /./ without appropriate validation, leading to ambiguous path resolution.
It is one branch of the family under CWE-41. It affects anything that screens a path as text before the file system or another program interprets it.
How it happens
A filter blocks a specific path, such as a protected file or a script, by matching it as a string. Insert /./ and the string differs while the target stays the same. The record's older examples are web servers: one allowed remote attackers to read source code for executable files by inserting a dot into the URL (CVE-2000-0004), and another allowed reading password protected files via /./ in the HTTP request (CVE-2002-0304). A third, CVE-1999-1083, is flagged in the record as possibly a cleansing error.
The newest example is a chain. CVE-2024-6091 is described by MITRE as a chain in which an AI agent platform does not restrict pathnames containing internal /./ sequences (CWE-55), leading to an incomplete denylist (CWE-184) that does not prevent OS command injection (CWE-78).
Read that carefully, because it shows how weaknesses stack. The platform tried to stop dangerous input with a denylist. The denylist missed variants containing /./. And what slipped through reached a place where it could be executed as a command. Three weaknesses, one bug.
A simplified picture of the denylist step:
# Vulnerable: denylist matches one spelling of a dangerous path
BLOCKED = ["/usr/bin/curl", "/bin/sh"]
def check(cmd_path):
if cmd_path in BLOCKED:
raise PermissionError()
return True
check("/usr/bin/./curl") # passes, runs the same binary
This is a sketch of the class of mistake, not the code of any product.
What an attacker gains
MITRE lists reading and modifying files and directories. The chain in CVE-2024-6091 shows the worst case when a path check guards command execution: running commands on the host. The older entries show source code disclosure and access to password protected files.
How to find it
- Look for any code that compares a path or a program name against a list, then passes it on.
- In a staging environment, take each blocked value and insert
/./at several positions, then see whether the block still holds. - For tools that run commands on behalf of an agent or a user, treat the command and its arguments as untrusted input and review them with the same suspicion as a web form.
- Check your dependencies. If an agent framework or server in your stack is affected, you want to know whether your code reaches the vulnerable function. Reachability analysis answers that.
Click through it at your own pace, or open it full screen.
How to fix it
MITRE's general guidance for this kind of input is the guide. Use an accept known good strategy, and do not rely exclusively on looking for malformed or malicious inputs, because that tends to miss at least one variant.
- Switch from a denylist to an allowlist. Name the exact commands or files that are permitted and refuse everything else.
- Canonicalize before comparing. Resolve the path with the platform API so
/./,..and repeated slashes disappear, then compare the result. - Compare resolved identities, not text, where you can, for example the real path of the executable.
- Do not pass untrusted strings to a shell. Use an argument list and a fixed program.
- Run with least privilege so that a miss does not become a host takeover.
import os, subprocess
ALLOWED = {os.path.realpath("/usr/bin/curl")}
def run(cmd_path, *args):
real = os.path.realpath(cmd_path)
if real not in ALLOWED:
raise PermissionError("not allowed")
return subprocess.run([real, *args], check=True) # no shell
If the vulnerable code is in a product you use, update it. You can check your dependency file in the free in-browser scan at gold.safeguard.sh/scan. It needs no login, the file stays on your machine and it handles up to 100 dependencies.
Quick check
Why does inserting /./ defeat a text filter?
It does not change the target, but it changes the string. A filter that matches an exact spelling sees something new, while the file system resolves to the same file.
What does CVE-2024-6091 teach about chains?
According to the record, a missed path variant (CWE-55) made a denylist incomplete (CWE-184), which then failed to stop OS command injection (CWE-78). Fixing the first link, by canonicalizing and using an allowlist, breaks the chain.
Is a better denylist the answer?
No. A longer list still misses variants. Use an allowlist and compare canonical paths.