Path traversal — File/Directory constructed with an interpolated, unvalidated path
ID |
dart.code.path_traversal |
Severity |
high |
Remediation Complexity |
hard |
Remediation Risk |
high |
Remediation Effort |
medium |
Resource |
Code |
Language |
Dart |
Description
A File / Directory / openRead / openWrite constructor is built by concatenating untrusted input into the path ($- interpolation or + concatenation). An attacker who controls any segment can use ../ to escape the intended directory and read or write outside it.
Rationale
Path-traversal in mobile apps is reachable from many sources: file
upload names, deeplink path parameters, downloaded archive
filenames, IPC payloads, sync metadata. When a tainted name flows
into File('${dir}/$untrusted') without normalization, the
attacker can supply ../../shared_prefs/secrets.xml (Android) or
similar paths to read app data, overwrite app code (less common
but possible in some sandboxes), or stage symlink races.
On Android specifically, this is the canonical vector for the
"Zip-Slip" pattern — an extracted ZIP entry with ../ segments
writing outside the intended extraction directory. On both
platforms, an attacker who can read arbitrary app files can
steal tokens, session state, and other secrets the app
legitimately writes elsewhere.
The following code illustrates a vulnerable pattern detected by this rule:
class PathTraversal {
Future<String> readWithInterp(String dir, String name) async {
// VULNERABLE: Path traversal — File/Directory constructed with an interpolated, unvalidated path
final f = File('$dir/$name');
return await f.readAsString();
}
Future<String> concatInterp(String dir, String userName) async {
final f = File(dir + userName);
return await f.readAsString();
}
}
Remediation
Use package:path to extract just the basename and normalize the
resolved path, then assert that the final path stays under the
expected directory. Reject any input containing .. segments,
null bytes, or absolute paths.
import 'package:path/path.dart' as p;
// Vulnerable
final f = File('${dir.path}/$userInput');
// Fixed
final safeName = p.basename(userInput); // strips ../
final candidate = p.normalize(p.join(dir.path, safeName));
if (!p.isWithin(dir.path, candidate)) {
throw const FormatException('path traversal attempt');
}
final f = File(candidate);
References
-
OWASP Top 10 2025 - A01 : Broken Access Control.
-
OWASP Top 10 2025 - A05 : Injection.
-
OWASP — Path Traversal — Generic attack reference;
../and equivalent encodings -
package:path (pub.dev) —
p.basename,p.normalize,p.isWithin— the safe-path helpers to use here