Supplies an absolute filesystem path (/etc/passwd) directly when the application concatenates user input without a base directory prefix.
TL;DR
/etc/passwd directly — bypasses ../ filters without any traversal sequencesos.path.join("/uploads/", "/etc/passwd") returns /etc/passwd — base is silently discardedurljoin discards base URL when path starts with /, creating SSRFrealpath — never filter input before joining, validate the resolved outputAbsolute path injection (CWE-36, a child of CWE-22) exploits a behavioral property of path join functions across multiple languages: when user input begins with an absolute path indicator (/ on Unix, C:\ on Windows), the language discards any previously-specified base directory and treats the user input as the complete path.
The attack requires no ../ sequences. A filter that successfully blocks all forms of relative traversal — including all encoding variants — remains fully exposed if it allows the user input to begin with /. From the attacker's perspective, this is the simplest possible payload: ?file=/etc/passwd. From the defender's perspective, it is the most frequently missed variant because developers assume a path that does not contain .. is safe.
Under OWASP A01:2021, this is a broken access control failure at the path construction layer. It is distinct from basic path traversal (CWE-22) in its root cause: relative traversal exploits insufficient canonicalization, while absolute injection exploits the language runtime's documented but surprising path join semantics.
The Python os.path.join behavior is the canonical example of this class:
import os
# Developer intent: restrict file access to /uploads/ directory
BASE = "/var/www/uploads"
def vulnerable_open(user_input):
# VULNERABLE: os.path.join discards BASE when user_input starts with /
path = os.path.join(BASE, user_input)
return open(path, "rb")
# Result of os.path.join("/var/www/uploads", "/etc/passwd")
# → "/etc/passwd" (BASE is completely discarded)
print(os.path.join("/var/www/uploads", "/etc/passwd"))
# /etc/passwd# Same behavior in Python's pathlib
from pathlib import Path
base = Path("/var/www/uploads")
result = base / "/etc/passwd"
print(result)
# /etc/passwdNode.js path.resolve has equivalent behavior:
const path = require('path');
// path.resolve discards all preceding arguments when an absolute path is encountered
console.log(path.resolve('/var/www/uploads', '/etc/passwd'));
// /etc/passwd
// path.join does NOT behave this way — it normalizes the full string
console.log(path.join('/var/www/uploads', '/etc/passwd'));
// /var/www/uploads/etc/passwd (note: still dangerous if not validated)urllib.parse.urljoin exposes the same class of vulnerability in URL construction:
from urllib.parse import urljoin
# When the second argument starts with /, the first URL's path is replaced
print(urljoin("https://api.internal/v1/reports/", "/etc/passwd"))
# https://api.internal/etc/passwd
# Attack in a microservice proxy context
base_url = "https://internal-api.corp/api/v2/"
user_path = "/admin/users"
# Result: https://internal-api.corp/admin/users
# The /api/v2/ prefix is silently discardedGET /proxy?path=/admin/users HTTP/1.1
Host: app.example.com
HTTP/1.1 200 OK
Content-Type: application/json
{"users": [...all users including admins...]}| Variant | Payload | Language Runtime | Impact |
|---|---|---|---|
| Unix absolute path | /etc/passwd | Python, PHP, Ruby | Credential file read |
| Proc environ | /proc/self/environ | Python, PHP | Runtime secrets — cloud keys, DB URLs |
| Windows absolute | C:\Windows\win.ini | PHP on Windows | Filesystem confirmation |
| Web config | C:\inetpub\wwwroot\web.config | ASP.NET on IIS | DB connection strings, app secrets |
| urljoin SSRF | /admin/reset via urljoin(base, user_input) | Python requests proxies | Internal API access |
| pathlib bypass | Pathlib / operator with absolute component | Python 3.x | Same as os.path.join |
| Java File bypass | new File("/uploads", "/etc/passwd") | Java (rare) | Absolute path — depends on implementation |
On Linux, the most impactful targets accessible via an absolute path without requiring elevated privileges:
/etc/passwd → user enumeration
/proc/self/environ → runtime secrets (cloud keys, DB URLs, API tokens)
/proc/self/cmdline → running process identification
/proc/version → kernel version for exploit selection
/app/.env → all application secrets in one file
/home/ubuntu/.ssh/id_rsa → SSH private key
/home/ubuntu/.aws/credentials → AWS credential file
/var/run/secrets/kubernetes.io/serviceaccount/token → Kubernetes JWTOn Windows, when the application runs under IIS:
C:\Windows\win.ini → filesystem access confirmation marker ([fonts])
C:\inetpub\wwwroot\web.config → ASP.NET connection strings, secret keys
C:\Windows\System32\drivers\etc\hosts → internal hostnamesCVE-2026-32871 — FastMCP OpenAPIProvider (CVSS 9.8): FastMCP's OpenAPI provider used urllib.parse.urljoin(base_url, user_path) to construct URLs for internal API proxying. When the user-supplied path began with /, urljoin discarded the base URL's path component. An authenticated attacker sent requests with paths starting with / to redirect the server's outbound HTTP requests to arbitrary internal endpoints — combining absolute path injection with SSRF. The forwarded requests included authentication headers from the original request, granting access to protected internal services.
CVE-2023-50164 — Apache Struts2 File Upload (CVSS 9.8): Apache Struts2 versions 2.0.0 through 6.3.0 were vulnerable to path traversal in file upload action parameters. Attackers manipulated the letter case of the upload path parameter to achieve an effective absolute file write — writing JSP webshells outside the intended upload directory. Unauthenticated exploitation achieved remote code execution. The vulnerability affected any Struts2 application using file upload functionality.
CVE-2023-34362 — MOVEit Transfer (CVSS 9.8): The mass exploitation of MOVEit Transfer by the Cl0p ransomware group combined SQL injection with a path traversal component that allowed arbitrary file read. The path traversal element used absolute path specifications to target configuration files outside the application's web root, enabling mass data theft affecting hundreds of organizations including government agencies and financial institutions.
/etc/passwd directly — no traversal sequences — and check the response body for root:x:0:0:.../, this test specifically reveals whether absolute path input is also filtered.C:\Windows\win.ini and look for [fonts] in the response./ to override the target path./var/run/secrets/kubernetes.io/serviceaccount/token — if returned, the application runs in a Kubernetes pod with service account token injection, and the JWT provides cluster API access./ and compare response sizes to test values that do not start with /.BreachVex includes "/{file}" as a dedicated absolute-path payload variant in its traversal payload set, specifically to test absolute path injection separately from relative traversal. Content gate validation applies the same three-criterion check: HTTP 200, body length ≥ 10 bytes, and file-specific marker regex match.
import os
BASE_DIR = os.path.realpath("/var/www/app/uploads")
def safe_open(user_input: str) -> bytes:
# Join first, then resolve — this neutralizes both relative and absolute injection
full_path = os.path.join(BASE_DIR, user_input)
resolved = os.path.realpath(full_path)
# os.path.realpath on os.path.join("/uploads", "/etc/passwd")
# → full_path = "/etc/passwd" (join discards base)
# → resolved = "/etc/passwd"
# → startsWith check FAILS → exception raised
if not resolved.startswith(BASE_DIR + os.sep):
raise PermissionError("Absolute path injection blocked")
with open(resolved, "rb") as f:
return f.read()// Node.js — path.resolve then validate
const path = require('path');
const fs = require('fs');
const BASE = fs.realpathSync('/var/www/uploads');
function safeRead(userInput) {
// path.resolve handles both absolute components and .. sequences
const resolved = path.resolve(BASE, userInput);
const real = fs.realpathSync(resolved);
if (!real.startsWith(BASE + path.sep)) {
throw new Error('Access denied: absolute path injection attempt');
}
return fs.readFileSync(real);
}Filtering / from the start of user input is not sufficient. Some applications only strip leading slashes, missing cases where the slash appears after encoding (%2f), or where the absolute path is constructed differently. The correct defense is to validate the resolved output — not to sanitize the input.
from urllib.parse import urljoin, urlparse
INTERNAL_BASE = "https://internal-api.corp/api/v2/"
def safe_proxy(user_path: str) -> str:
# Reject paths that start with / to prevent urljoin base override
if user_path.startswith("/"):
raise ValueError("Absolute path not permitted in proxy parameter")
# Additionally: allowlist path prefixes
allowed_prefixes = ["reports/", "documents/", "exports/"]
if not any(user_path.startswith(p) for p in allowed_prefixes):
raise ValueError("Path not in allowed prefix list")
return urljoin(INTERNAL_BASE, user_path)Replace user-controlled path parameters with opaque identifiers that map to server-side paths:
import uuid
from typing import Optional
# File registry: UUID → real path
FILE_REGISTRY: dict[str, str] = {}
def register_file(real_path: str) -> str:
file_id = str(uuid.uuid4())
FILE_REGISTRY[file_id] = real_path
return file_id
def get_file(file_id: str) -> Optional[bytes]:
path = FILE_REGISTRY.get(file_id)
if path is None:
return None
with open(path, "rb") as f:
return f.read()Absolute path injection (CWE-22, CWE-36) occurs when an application passes user-supplied input directly to a filesystem function without prepending a fixed base directory, or when the language's path join function silently discards the base if the user input starts with /. The attacker supplies a complete path like /etc/passwd without needing any ../ traversal sequences.
Python's os.path.join behavior is documented but surprising: if any component is an absolute path, all previous components are discarded. os.path.join('/uploads/', '/etc/passwd') returns '/etc/passwd', not '/uploads//etc/passwd'. A filter that strips ../ sequences but allows / at the start of user input fails against this variant.
Basic path traversal uses ../ sequences to navigate upward from a constrained base directory. Absolute path injection skips the base entirely by supplying a complete path. A server-side filter that blocks ../ but accepts / at the start of user input is vulnerable to absolute injection but not to basic traversal. Both require separate tests.
CVE-2023-50164 (Apache Struts2, CVSS 9.8) used case manipulation in upload parameters to achieve path traversal equivalent to absolute file write. CVE-2026-32871 (FastMCP, CVSS 9.8) exploited urllib.parse.urljoin behavior — when the user-supplied path starts with /, the base URL is discarded, directing server requests to attacker-controlled paths via SSRF. CVE-2023-34362 (MOVEit, CVSS 9.8) chained SQL injection with path traversal to access arbitrary files.
CWE-36 (Absolute Path Traversal) is the specific identifier for this variant. It is a child of CWE-22 (Improper Limitation of a Pathname to a Restricted Directory) in the MITRE taxonomy. Both CWEs are relevant when cataloging findings.
Submit /etc/passwd directly as the parameter value (without any ../ sequences). Check the response for the marker root:x:0:0:. On Windows, try C:\Windows\win.ini and look for [fonts]. Also test /proc/self/environ — confirmation is any key=value pair like PATH= or DATABASE_URL=. If ../ payloads fail but /etc/passwd succeeds, the application filters traversal sequences but not absolute paths.
path.join() in Node.js does not discard previous components for absolute paths — it normalizes the full joined string. However, path.resolve() follows Python-like behavior and returns the absolute path of the last absolute component. Applications using path.resolve(BASE, userInput) where userInput starts with / are vulnerable. Always validate the result of path.resolve() with startsWith(base + path.sep).
urllib.parse.urljoin(base_url, user_path) discards everything in base_url after the last / when user_path starts with /. This means urljoin('https://api.internal/v1/', '/etc/passwd') produces 'https://api.internal/etc/passwd' — ignoring the /v1/ prefix. In microservice architectures where internal APIs proxy user-supplied paths, this creates SSRF and path traversal simultaneously.