flawopen.com/path-traversal/Swift
Path Traversal in Swift (Foundation URL, Vapor)
URL.appendingPathComponent keeps ../ segments for the file system to follow, a hasPrefix check on standardizedFileURL without a trailing slash accepts /srv/uploads-old and ignores symbolic links, and a file name taken from a server response can climb out of an app's Documents folder. How each mistake exposes files from a Vapor server or an iOS or macOS app, and how resolvingSymlinksInPath, a slash-terminated prefix check and lastPathComponent stop it.
Imagine an iPhone photo album where every download belongs in one private folder. URL.appendingPathComponent() adds a name, but ../ can walk out of that folder. standardizedFileURL cleans the spelling; it does not follow a symbolic-link shortcut, and hasPrefix on /srv/uploads also accepts the neighboring /srv/uploads-old. Resolve both base and target with resolvingSymlinksInPath(), then require hasPrefix(base.path + "/"). For a server-supplied filename, keep only lastPathComponent and write with .withoutOverwriting. Vapor FileMiddleware is the safer choice for public static files.
Core Concepts & Subsystem Terms
appendingPathComponent keeps ..uploadDir.appendingPathComponent("../../etc/hostname")keeps the..segments, and the kernel resolves them when the file is opened. An absolute name is appended rather than replacing the base, so..is the attack.standardizedFileURL and hasPrefixstandardizedFileURLremoves.and..lexically, butpath.hasPrefix("/srv/uploads")compares characters, so/srv/uploads-old/secret.txtpasses, and a symbolic link inside the folder is not followed at all.resolvingSymlinksInPath()- Resolves symbolic links in the existing parts of the path. Resolve the base the same way, then require the target to start with the base path plus
/, so links cannot lead outside the folder. lastPathComponent for server-supplied names- A download's name from a
Content-Dispositionheader or JSON field can contain../.URL(fileURLWithPath: name).lastPathComponentkeeps only the final component; writing with.withoutOverwritingrefuses to replace an existing file. Vapor FileMiddleware- Serves files from the
Publicdirectory and rejects request paths that contain../. Use it for static assets instead of building file paths fromreq.parametersorreq.queryyourself.
Framework mappings
- CWE reference for the weakness described in this guide: CWE-22
Step-by-Step Attack Flow
An untrusted name reaches a file API
A Vapor route reads file from req.query, or an app receives a file name in a server response, and appends it to a directory URL.
The name leaves the intended folder
It contains ../ segments, names a sibling folder with the same prefix, or reaches a symbolic link. The benign test value ../../etc/hostname illustrates the read case.
Foundation opens the resolved path
streamFile, String(contentsOf:) or Data.write(to:) passes the path to the kernel, which resolves every .. and link.
Files leak or are overwritten
A server leaks configuration and keys; an app can have its database, cached credentials or configuration in Application Support overwritten by a malicious download.
Source Code: Flaw vs. Secure Implementation
// Files.swift: each function lets an untrusted name leave its folder
import Foundation
import Vapor
let uploadDir = URL(fileURLWithPath: "/srv/uploads", isDirectory: true)
// appendingPathComponent keeps ../ and the kernel follows it
func download(_ req: Request) throws -> Response {
let name = try req.query.get(String.self, at: "file")
return req.fileio.streamFile(at: uploadDir.appendingPathComponent(name).path)
}
// hasPrefix without / accepts /srv/uploads-old and ignores symlinks
func preview(_ req: Request) throws -> String {
let name = try req.query.get(String.self, at: "file")
let path = uploadDir.appendingPathComponent(name).standardizedFileURL.path
guard path.hasPrefix(uploadDir.path) else { throw Abort(.notFound) }
return try String(contentsOfFile: path, encoding: .utf8)
}
// a server name such as ../../Library/Preferences/app.plist overwrites app data
func saveAttachment(serverName: String, data: Data, in dir: URL) throws -> URL {
let target = dir.appendingPathComponent(serverName)
try data.write(to: target)
return target
}
// Files.swift: standardize, resolve links, compare with a trailing slash
import Foundation
import Vapor
let uploadDir = URL(fileURLWithPath: "/srv/uploads", isDirectory: true).resolvingSymlinksInPath()
// base and target are resolved the same way; the slash stops sibling folders
func inside(_ base: URL, _ name: String) -> URL? {
let target = base.appendingPathComponent(name).standardizedFileURL.resolvingSymlinksInPath()
guard target.path.hasPrefix(base.path + "/") else { return nil }
return target
}
// downloads go through the containment check
func download(_ req: Request) throws -> Response {
let name = try req.query.get(String.self, at: "file")
guard let url = inside(uploadDir, name) else { throw Abort(.notFound) }
return req.fileio.streamFile(at: url.path)
}
// previews use the same check
func preview(_ req: Request) throws -> String {
let name = try req.query.get(String.self, at: "file")
guard let url = inside(uploadDir, name) else { throw Abort(.notFound) }
return try String(contentsOf: url, encoding: .utf8)
}
// only the last component is used and existing files are never replaced
func saveAttachment(serverName: String, data: Data, in dir: URL) throws -> URL {
let base = dir.resolvingSymlinksInPath()
guard let target = inside(base, URL(fileURLWithPath: serverName).lastPathComponent) else {
throw CocoaError(.fileWriteInvalidFileName)
}
try data.write(to: target, options: .withoutOverwriting)
return target
}
Engineering & System Hardening Checklist
- ✓Add a SwiftLint
custom_rulesregex or Semgrep rule that flagsappendingPathComponentandappending(path:)fed fromreq.query,req.parametersor decoded server responses. - ✓Route every user-influenced name through one function that standardizes, resolves symlinks and requires
hasPrefix(base.path + "/"). - ✓Serve public assets with Vapor's
FileMiddlewarerather than hand-written file routes. - ✓Reduce server-supplied names with
lastPathComponentor generate names, and write with.withoutOverwriting. - ✓Run the server as an unprivileged user, and keep secrets out of any directory an app or server exposes to requests.