Safeguard
Application Security•

CWE-57 Path Equivalence: fakedir/../realdir/filename Bypass

Prefix a path with a fake directory and a dot dot, and a check for the real one may never fire. CWE-57 with causes and fixes.

Safeguard Security Team
5 min read

Take a protected file at realdir/filename. Now ask for fakedir/../realdir/filename. The two paths point at the same file, but a rule that looks only for the first spelling will not see the second. That is CWE-57.

MITRE describes it this way: the product has protection mechanisms to restrict access to realdir/filename, but it builds pathnames from external input in the form fakedir/../realdir/filename, and those mechanisms do not handle them. Attackers can then act on the file despite the protection.

It sits in the family under CWE-41. It is not the classic traversal that escapes a directory. The target is inside the protected area. The dot dot segment only exists to change the spelling.

Who is affected? Any product that guards paths by text: web servers, proxies, file services, application firewalls and custom access checks.

How it happens

The mistake is a check that runs before canonicalization. A guard asks whether the request path begins with a protected prefix, or equals a protected name, or matches a pattern. The request arrives in a different form, so the guard says no. Later a component resolves the dot dot segment and opens the protected file.

The record gives three examples:

  • A proxy allowed remote attackers to bypass denylist restrictions and connect to unauthorized web servers by modifying the requested URL, including a double slash and a /SUBDIR/.. segment where the desired page was named (CVE-2001-1152).
  • An application checked access for a restricted URL before canonicalization (CVE-2000-0191).
  • CGI source disclosure using dirname/../cgi-bin (CVE-2005-1366).

The second entry states the root cause in one line: the check happened before canonicalization.

A small vulnerable example:

# Vulnerable: authorization on raw text, resolution later
def handler(path):
    if path.startswith("/admin") and not user.is_admin:
        return deny()
    full = os.path.normpath(os.path.join(DOCROOT, path.lstrip("/")))
    return send(full)

# handler("/public/../admin/users.csv") passes the check
# and normpath resolves it to /admin/users.csv

What an attacker gains

MITRE lists reading and modifying files and directories. The recorded cases include bypassing a denylist to reach unauthorized servers through a proxy and disclosing CGI source. Anything protected by a path rule is exposed to the same trick, including administrative files, exports and configuration.

How to find it

The record names no detection technique, so use review and testing.

  1. List every access control that depends on a path or URL string.
  2. For each, decide where canonicalization happens relative to the check. If the check comes first, flag it.
  3. In a staging environment, take each protected path and request it with a fake directory and a dot dot segment inserted in front. Try encoded forms too, for instance with the slash percent encoded.
  4. Test through every layer, including any proxy or gateway in front of the application, because each layer may normalize on its own schedule.
  5. Add these requests to your regression tests so that a later refactor cannot quietly reintroduce the gap.

How to fix it

Canonicalize, then authorize. The remedy is mainly about ordering.

  1. Resolve the path to its canonical form with the platform API.
  2. Confirm the canonical path is inside the allowed root.
  3. Apply the access rule to the canonical path, not to the input.
  4. Open exactly the string you authorized. Do not re-resolve a different value afterward.
  5. Use deny by default for protected areas, and enforce permissions in the file system as well.
  6. Where input should never contain dot segments, reject it.
import os

DOCROOT = os.path.realpath("/srv/www")

def handler(path):
    full = os.path.realpath(os.path.join(DOCROOT, path.lstrip("/")))
    if os.path.commonpath([DOCROOT, full]) != DOCROOT:
        return deny()
    rel = os.path.relpath(full, DOCROOT)
    if (rel == "admin" or rel.startswith("admin" + os.sep)) and not user.is_admin:
        return deny()
    return send(full)

Note that the check and the open use the same full value. If you check one string and open another, you have rebuilt the original bug.

MITRE's wider guidance for input handling applies as well: prefer an accept known good approach, and do not depend on filtering that removes dangerous sequences, which behaves like an incomplete denylist.

For proxies and servers you did not write, updating is the fix. A free check of your dependency file is available at gold.safeguard.sh/scan. No login is needed, the file stays on your machine and it handles up to 100 dependencies.

Quick check

How does CWE-57 differ from classic path traversal?

The target stays inside the protected directory. The fake directory and dot dot segment only change how the path is spelled so that a rule written for the plain spelling does not match.

What single ordering rule prevents it?

Canonicalize first, then authorize, then open the same canonical value.

Why test through the proxy as well?

A proxy and the server behind it may normalize differently. The record includes a proxy case where a modified URL got around denylist restrictions.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.