Skip to content
Access control Reviewed 2026-09-12

File Path Injection

What does this mean ?

Path traversal occurs when an application accepts a path that reaches outside the intended storage boundary. Parent-directory segments, absolute paths, platform-specific names and symbolic links can all affect resolution. Normalizing a path removes redundant syntax; it does not, by itself, authorize the resulting location.

What can happen ?

Depending on the operation, the application may expose, overwrite or remove unrelated files. A file inside the correct directory may still belong to another user, so filesystem containment and object authorization are separate requirements.

Recommendation

Prefer an opaque file ID looked up in server-owned metadata, with access checked for the current user. Keep the stored filename separate from the original display name. For direct paths, reject unexpected path forms and use an API that confines file operations to an approved root.

A string prefix test without a directory boundary can confuse /data/reports with /data/reports-old. Resolving symlinks before opening a file still creates a race if an attacker can change the directory in between. Keep storage directories under application control; where hostile filesystem changes are possible, use descriptor-relative traversal-resistant APIs. Test the operating systems actually deployed.

Sample Code

All examples assume file authorization is applied before the helper is called. They perform reads rather than demonstrating destructive file operations.

For an application-owned directory with no untrusted symlinks or reparse points, use server-selected basenames. This helper accepts only known IDs, never arbitrary paths:

// Unsafe: a client-provided path is passed directly to the filesystem.
byte[] unsafeBytes = File.ReadAllBytes(requestedPath);
static byte[] ReadPublicGuide(string guideId)
{
    string fileName = guideId switch
    {
        "getting-started" => "getting-started.pdf",
        "integration" => "integration.pdf",
        _ => throw new ArgumentException("Unknown guide", nameof(guideId))
    };
    return File.ReadAllBytes(Path.Combine("/srv/public-guides", fileName));
}

For per-user files, use an authorization-checked database lookup instead of a public-guide switch. Do not invert the reject condition: a path outside the permitted root must never reach the file operation.

Python 3, for a directory whose contents cannot be concurrently changed by an attacker:

from pathlib import Path

def read_report(root: Path, requested: str) -> bytes:
    base = root.resolve(strict=True)
    relative = Path(requested)
    if relative.is_absolute():
        raise ValueError("Relative path required")
    candidate = (base / relative).resolve(strict=True)
    if candidate == base or not candidate.is_relative_to(base):
        raise ValueError("Path outside report directory")
    return candidate.read_bytes()

Unsafe code would read Path(requested) directly. This helper follows symlinks during resolution and checks their destination, but is not a defense against a symlink swap after the check. Bound file size in a real download handler.

Java NIO, under the same application-owned filesystem assumption:

static byte[] readReport(java.nio.file.Path root, String requested)
        throws java.io.IOException {
    var base = root.toRealPath();
    var relative = java.nio.file.Path.of(requested);
    if (relative.isAbsolute()) {
        throw new IllegalArgumentException("Relative path required");
    }
    var candidate = base.resolve(relative).toRealPath();
    if (candidate.equals(base) || !candidate.startsWith(base)) {
        throw new IllegalArgumentException("Path outside report directory");
    }
    return java.nio.file.Files.readAllBytes(candidate);
}

Path.startsWith compares path components. The check must precede the read. Use a SecureDirectoryStream where supported if untrusted concurrent changes are in scope.

Go 1.24 introduced os.OpenRoot. For a native server platform supported by this API:

// Unsafe: filepath.Join/Clean alone does not enforce containment.
file, err := os.Open(filepath.Join(baseDirectory, requested))
func openReport(baseDirectory, requested string) (*os.File, error) {
    root, err := os.OpenRoot(baseDirectory)
    if err != nil {
        return nil, err
    }
    defer root.Close()
    return root.Open(requested)
}

The caller closes the returned file. os.Root rejects escapes through parent paths and symlinks; platform-specific limitations still apply, including different guarantees for GOOS=js. It does not decide which customer may read a file or limit file size.

Regression checks

Create a temporary fixture root and sibling directory. Confirm a normal child can be read and a sibling path or absolute path is rejected. Test a symlink that points outside the root on supported platforms. Include a sibling whose name shares the root's prefix. Verify that another user's valid file ID is denied independently of these tests. Never use real system or customer files for regression fixtures.

References