$npx -y skills add utkusen/sast-skills --skill sast-pathtraversalDetect path traversal vulnerabilities in a codebase using a three-phase approach: recon (find file-loading sinks with dynamic paths), batched verify (trace user input and mitigations in parallel subagents, 3 sinks each), and merge (consolidate batch results). Requires sast/archit
| 1 | # Path Traversal Detection |
| 2 | |
| 3 | You are performing a focused security assessment to find path traversal vulnerabilities in a codebase. This skill uses a three-phase approach with subagents: **recon** (find file-loading sinks with dynamic paths), **batched verify** (trace user input and check mitigations in parallel batches of 3), and **merge** (consolidate batch results into one report). |
| 4 | |
| 5 | **Prerequisites**: `sast/architecture.md` must exist. Run the analysis skill first if it doesn't. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## What is Path Traversal |
| 10 | |
| 11 | Path traversal (also called directory traversal) occurs when user-supplied input is incorporated into a file path that is then used to read, write, or serve files from the filesystem — without properly constraining the resulting path to an intended base directory. An attacker can supply sequences like `../` or encoded variants (`%2e%2e%2f`, `..%2f`, `%2e%2e/`) to escape the intended directory and access arbitrary files such as `/etc/passwd`, application source code, credentials, or private keys. |
| 12 | |
| 13 | The core pattern: *unvalidated user input reaches a filesystem operation and the resolved path is not verified to remain within the intended base directory.* |
| 14 | |
| 15 | ### What Path Traversal IS |
| 16 | |
| 17 | - Serving a user-requested filename directly from a base directory without canonicalizing and checking the resulting path: |
| 18 | `open(os.path.join(BASE_DIR, user_filename))` |
| 19 | - Constructing a file path from a URL parameter and passing it to a file-read function: |
| 20 | `fs.readFile(path.join(__dirname, req.query.file), ...)` |
| 21 | - Template rendering or include directives driven by user input: |
| 22 | `include($_GET['page'] . '.php')` |
| 23 | - Archive extraction (`ZipFile`, `tarfile`, `zipslip`) where entry names are used as output paths without stripping `../` components |
| 24 | - Using `send_file()` / `send_from_directory()` / `res.sendFile()` with an unsanitized user-controlled path |
| 25 | - Reading a file whose path is derived from a user-controlled database value that was stored without sanitization |
| 26 | |
| 27 | ### What Path Traversal is NOT |
| 28 | |
| 29 | Do not flag these as path traversal: |
| 30 | |
| 31 | - **SSRF**: Fetching a remote URL from user input — that is Server-Side Request Forgery, a separate class |
| 32 | - **RCE via file write**: Writing attacker-controlled content to an arbitrary path — related but a different impact class (flag as RCE or File Upload) |
| 33 | - **Static file serving**: Serving files from a path that is entirely hardcoded with no user influence |
| 34 | - **Safe path joins followed by realpath + prefix check**: The code computes `realpath()` and verifies it starts with the intended base directory |
| 35 | - **basename() before join**: Using only the filename component strips traversal sequences (though note this prevents directory selection, not just traversal) |
| 36 | |
| 37 | ### Patterns That Prevent Path Traversal |
| 38 | |
| 39 | When you see these mitigations applied **before** the file operation, the code is likely **not vulnerable**: |
| 40 | |
| 41 | **1. `realpath` / `resolve` followed by a base-directory prefix check (most robust fix)** |
| 42 | ```python |
| 43 | # Python |
| 44 | import os |
| 45 | BASE = '/var/www/files' |
| 46 | safe_path = os.path.realpath(os.path.join(BASE, user_input)) |
| 47 | if not safe_path.startswith(BASE + os.sep): |
| 48 | raise PermissionError("Path escape detected") |
| 49 | with open(safe_path) as f: |
| 50 | ... |
| 51 | ``` |
| 52 | |
| 53 | ```javascript |
| 54 | // Node.js |
| 55 | const BASE = path.resolve('/var/www/files'); |
| 56 | const resolved = path.resolve(BASE, req.query.file); |
| 57 | if (!resolved.startsWith(BASE + path.sep)) { |
| 58 | return res.status(403).send('Forbidden'); |
| 59 | } |
| 60 | fs.readFile(resolved, ...); |
| 61 | ``` |
| 62 | |
| 63 | ```java |
| 64 | // Java |
| 65 | Path base = Paths.get("/var/www/files").toRealPath(); |
| 66 | Path resolved = base.resolve(userInput).normalize(); |
| 67 | if (!resolved.startsWith(base)) { |
| 68 | throw new SecurityException("Path escape"); |
| 69 | } |
| 70 | Files.readAllBytes(resolved); |
| 71 | ``` |
| 72 | |
| 73 | **2. `basename()` / `path.basename()` to strip directory components** |
| 74 | ```python |
| 75 | # Python — strips all directory parts, only the filename remains |
| 76 | filename = os.path.basename(user_input) |
| 77 | with open(os.path.join(BASE, filename)) as f: |
| 78 | ... |
| 79 | ``` |
| 80 | |
| 81 | ```php |
| 82 | // PHP |
| 83 | $filename = basename($_GET['file']); |
| 84 | readfile('/var/www/uploads/' . $filename); |
| 85 | ``` |
| 86 | |
| 87 | **3. Allowlist of permitted filenames or extensions** |
| 88 | ```python |
| 89 | ALLOWED = {'report.pdf', 'manual.txt', 'logo.png'} |
| 90 | if user_input not in ALLOWED: |
| 91 | abort(400) |
| 92 | with open(os.path.join(BASE, user_input)) as f: |
| 93 | ... |
| 94 | ``` |
| 95 | |
| 96 | **4. Framework-provided safe file serving** |
| 97 | ```python |
| 98 | # Flask — send_from_directory validates the path stays within the directory |
| 99 | return send_from_directory('/var/www/files', filename) |
| 100 | |
| 101 | # Django — FileResponse with a pa |