Skip to content
cccskills
FreePublished on GitHub

path-traversal-and-file-handling

Use when the diff builds a filesystem path from input, extracts an archive, accepts an upload, serves a file or writes a temp file - traversal, symlinks, zip-slip, upload type and served content-type

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md6.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Path Traversal and File Handling

Overview

Join, normalize, resolve and clean are lexical: they rewrite a string, they do not ask the filesystem where it points. A path check that runs on the string and an open that runs on the filesystem disagree whenever there is a .., an absolute path, or a symlink.

Core principle: resolve the real path first, then check containment by path components, then open what you checked — or use an API that confines the open itself.

1. Traversal (CWE-22)

// ❌ HasPrefix on a cleaned string: "/srv/files-private/x" passes for base "/srv/files",
//    and a symlink inside base escapes entirely
p := filepath.Join(base, name)
if !strings.HasPrefix(filepath.Clean(p), base) { return errBadPath }
f, _ := os.Open(p)

// ✅ Go 1.24+: the root confines every lookup, symlinks included
root, _ := os.OpenRoot(base)
f, err := root.Open(name)
// older Go: reject non-local names first — filepath.IsLocal(name) (Go 1.20+)
// ❌ normalize() is lexical; a symlink under base still escapes
Path p = base.resolve(name).normalize();
if (!p.startsWith(base)) throw new SecurityException();

// ✅ compare real paths (Path.startsWith is component-wise, String.startsWith is not)
Path real = base.resolve(name).toRealPath();
if (!real.startsWith(base.toRealPath())) throw new SecurityException();
# ❌ os.path.join drops base entirely when name is absolute ("/etc/passwd")
path = os.path.join(UPLOAD_DIR, name)

# ✅
root = Path(UPLOAD_DIR).resolve()
path = (root / name).resolve()
if not path.is_relative_to(root):   # Python 3.9+
    abort(400)
// ❌ startsWith without a trailing separator; "/data/app-secrets" passes for "/data/app"
const p = path.resolve(base, name); if (!p.startsWith(base)) throw ...
// ✅
const p = path.resolve(base, name); if (!p.startsWith(base + path.sep)) throw ...
// and for symlinks: fs.realpathSync(p) before the check

Flask: send_from_directory. Express: res.sendFile(name, { root: base }) (rejects escaping paths) rather than res.sendFile(path.join(base, name)).

2. Symlinks (CWE-59)

Lexical checks never see a symlink. It matters when the attacker can create files inside the base (an upload directory, an extracted archive, a cloned repository, a shared volume). Resolve with realpath/EvalSymlinks/toRealPath, or open with O_NOFOLLOW / os.Root. A check-then-open gap on a directory the attacker writes to is a race (CWE-367) — report it only when the attacker demonstrably controls that directory.

3. Archive Extraction (zip-slip)

Archive entry names are attacker input: ../../.ssh/authorized_keys, absolute paths, and symlink entries that point outside and are then written through.

// ❌
File out = new File(destDir, entry.getName());
Files.copy(zis, out.toPath());
// ✅
Path out = destDir.resolve(entry.getName()).normalize();
if (!out.startsWith(destDir)) throw new IOException("bad entry: " + entry.getName());
  • Python tarfile.extractall() without filter="data" (the safe default only from Python 3.14); shutil.unpack_archive on tar. zipfile.extractall sanitises .. and absolute names — not a finding by itself.
  • Go archive/zip and archive/tar: check filepath.IsLocal(hdr.Name) and skip or reject symlink entries.
  • Node: hand-rolled extraction with adm-zip/unzipper/yauzl joining entry.path.

Decompression bombs are a resource issue — excluded, except a bomb expanded before any size check on a shared worker by an unauthenticated or low-trust sender (security-hard-exclusions-and-precedents §4).

4. Uploads (CWE-434)

  • Where it is stored: under a web root or a directory the server executes from (PHP, JSP, CGI) → upload becomes code. Store outside, under a generated name.
  • What name it gets: the client filename used in the path → traversal (section 1). Use a server-generated id; keep the original name only as metadata.
  • What type it is served as: an uploaded .html or .svg served inline from the app's own origin is stored XSS. Serve user files with Content-Disposition: attachment, a fixed safe Content-Type, X-Content-Type-Options: nosniff, or from a separate cookieless domain.
  • How the type is checked: the client's Content-Type and the extension are claims. Allowlist extensions and verify the magic bytes when the type decides how the file is processed (image libraries, document parsers).
  • Size: unlimited size is resource exhaustion — excluded; do not report it.

5. Serving and Static Routes

  • A static mapping widened to the project root exposes .env, .git/, source and backups.
  • A download endpoint that takes a full path or an object-storage key from the client — combine with access-control-and-idor: can user A fetch tenants/B/report.pdf?
  • Signed URLs whose key is built from user input without the tenant prefix.

6. Temporary Files (CWE-377)

Predictable names in a shared temp directory (/tmp/export- + userId) let a local attacker pre-create or symlink the file. Use os.CreateTemp, tempfile.mkstemp/NamedTemporaryFile, Files.createTempFile, fs.mkdtemp. Only a finding when other local users or tenants share the machine.

Severity Guide

Reading arbitrary server files or writing outside the intended directory → HIGH; CRITICAL when the write reaches code, config or authorized_keys, or the read reaches credentials. Inline-served HTML/SVG upload → HIGH (stored XSS).

Common Mistakes

  • Reporting traversal where the value is a server constant, an enum, or a UUID parsed before use.
  • Reporting path traversal in browser or mobile client code — it is the user's own filesystem.
  • Trusting filepath.Clean, path.normalize or os.path.normpath as a containment check.
  • Missing that Flask's default <name> converter rejects /, while <path:name> accepts it.

Red Flags

  • Join/resolve followed by a string startsWith/HasPrefix check.
  • entry.getName(), hdr.Name, member.name used to build an output path.
  • extractall( with no filter= on Python < 3.14.
  • An upload handler that writes file.originalname / filename into the path.
  • Content-Type taken from the upload when serving it back.

References (names and links only)

CWE-22, CWE-59, CWE-367, CWE-377, CWE-434 at cwe.mitre.org · OWASP Top 10:2025 A01 Broken Access Control · ASVS 5.0 V5 File Handling

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Use when writing acceptance criteria for a task - express each as an observable Given/When/Then that QA can execute, including negative cases

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

Use when the diff adds or changes an endpoint, resolver, RPC, job or query that takes an object id, a role check, a request binding or a tenant filter - BOLA/IDOR, function-level authorization, mass assignment and tenant scoping

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

Use on every UI change - semantic HTML, labels for controls, keyboard-navigable dialogs/menus, visible focus, and never color as the only signal

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

Use when a task changes any screen, form, dialog, menu or control - Lighthouse/axe scan of the changed screens, a keyboard walk, and the thresholds that fail a task

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

How to work a task returned with review, QA or UAT findings. Use when a task is in need_revision or PR review comments are in your context.

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

Use when the repository keeps its alerting in code, or a person asks for alerts outside TaskTrooper - PrometheusRule YAML, a Cloud Monitoring alert policy in Terraform, a Datadog monitor, each routed to TaskTrooper's alert ingest URL so it opens an incident here

The English summary is being prepared. Showing the original description.

makifbaysal/tasktrooper118Updated Oct 21, 2026

See all skills by makifbaysal

Report a problem with this skill