Merge pull request #11446 from atorralba/atorralba/swift/path-injection

Swift: Add path injection query
This commit is contained in:
Mathias Vorreiter Pedersen
2022-12-06 11:03:26 +00:00
committed by GitHub
12 changed files with 471 additions and 0 deletions

View File

@@ -80,13 +80,16 @@ private import internal.FlowSummaryImplSpecific
private module Frameworks {
private import codeql.swift.frameworks.StandardLibrary.CustomUrlSchemes
private import codeql.swift.frameworks.StandardLibrary.Data
private import codeql.swift.frameworks.StandardLibrary.FilePath
private import codeql.swift.frameworks.StandardLibrary.InputStream
private import codeql.swift.frameworks.StandardLibrary.NSData
private import codeql.swift.frameworks.StandardLibrary.NsUrl
private import codeql.swift.frameworks.StandardLibrary.String
private import codeql.swift.frameworks.StandardLibrary.Url
private import codeql.swift.frameworks.StandardLibrary.UrlSession
private import codeql.swift.frameworks.StandardLibrary.WebView
private import codeql.swift.frameworks.Alamofire.Alamofire
private import codeql.swift.security.PathInjection
}
/**

View File

@@ -0,0 +1,17 @@
import swift
private import codeql.swift.dataflow.ExternalFlow
/** The struct `FilePath`. */
class FilePath extends StructDecl {
FilePath() { this.getFullName() = "FilePath" }
}
/**
* A model for `FilePath` members that permit taint flow.
*/
private class FilePathSummaries extends SummaryModelCsv {
override predicate row(string row) {
// TODO: Properly model this class
row = ";FilePath;true;init(stringLiteral:);(String);;Argument[0];ReturnValue;taint"
}
}

View File

@@ -0,0 +1,12 @@
import swift
private import codeql.swift.dataflow.ExternalFlow
/**
* A model for `NSURL` members that permit taint flow.
*/
private class NsUrlSummaries extends SummaryModelCsv {
override predicate row(string row) {
// TODO: Properly model this class
row = ";NSURL;true;init(string:);(String);;Argument[0];ReturnValue;taint"
}
}

View File

@@ -0,0 +1,110 @@
/** Provides classes and predicates to reason about path injection vulnerabilities. */
import swift
private import codeql.swift.controlflow.BasicBlocks
private import codeql.swift.controlflow.ControlFlowGraph
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.ExternalFlow
private import codeql.swift.dataflow.TaintTracking
private import codeql.swift.generated.ParentChild
private import codeql.swift.frameworks.StandardLibrary.FilePath
/** A data flow sink for path injection vulnerabilities. */
abstract class PathInjectionSink extends DataFlow::Node { }
/** A sanitizer for path injection vulnerabilities. */
abstract class PathInjectionSanitizer extends DataFlow::Node { }
/**
* A unit class for adding additional taint steps.
*
* Extend this class to add additional taint steps that should apply to paths related to
* path injection vulnerabilities.
*/
class PathInjectionAdditionalTaintStep extends Unit {
/**
* Holds if the step from `node1` to `node2` should be considered a taint
* step for paths related to path injection vulnerabilities.
*/
abstract predicate step(DataFlow::Node node1, DataFlow::Node node2);
}
private class DefaultPathInjectionSink extends PathInjectionSink {
DefaultPathInjectionSink() { sinkNode(this, "path-injection") }
}
private class DefaultPathInjectionSanitizer extends PathInjectionSanitizer {
DefaultPathInjectionSanitizer() {
// This is a simplified implementation.
// TODO: Implement a complete path sanitizer when Guards are available.
exists(CallExpr starts, CallExpr normalize, DataFlow::Node validated |
starts.getStaticTarget().getName() = "starts(with:)" and
starts.getStaticTarget().getEnclosingDecl() instanceof FilePath and
normalize.getStaticTarget().getName() = "lexicallyNormalized()" and
normalize.getStaticTarget().getEnclosingDecl() instanceof FilePath
|
TaintTracking::localTaint(validated, DataFlow::exprNode(normalize.getQualifier())) and
DataFlow::localExprFlow(normalize, starts.getQualifier()) and
DataFlow::localFlow(validated, this) and
exists(ConditionBlock bb, SuccessorTypes::BooleanSuccessor b |
bb.getANode().getNode().asAstNode().(IfStmt).getACondition() = getImmediateParent*(starts) and
b.getValue() = true
|
bb.controls(this.getCfgNode().getBasicBlock(), b)
)
)
}
}
private class PathInjectionSinks extends SinkModelCsv {
override predicate row(string row) {
row =
[
";Data;true;write(to:options:);;;Argument[0];path-injection",
";NSData;true;write(to:atomically:);;;Argument[0];path-injection",
";NSData;true;write(to:options:);;;Argument[0];path-injection",
";NSData;true;write(toFile:atomically:);;;Argument[0];path-injection",
";NSData;true;write(toFile:options:);;;Argument[0];path-injection",
";FileManager;true;contentsOfDirectory(at:includingPropertiesForKeys:options:);;;Argument[0];path-injection",
";FileManager;true;contentsOfDirectory(atPath:);;;Argument[0];path-injection",
";FileManager;true;enumerator(at:includingPropertiesForKeys:options:errorHandler:);;;Argument[0];path-injection",
";FileManager;true;enumerator(atPath:);;;Argument[0];path-injection",
";FileManager;true;subpathsOfDirectory(atPath:);;;Argument[0];path-injection",
";FileManager;true;subpaths(atPath:);;;Argument[0];path-injection",
";FileManager;true;createDirectory(at:withIntermediateDirectories:attributes:);;;Argument[0];path-injection",
";FileManager;true;createDirectory(atPath:withIntermediateDirectories:attributes:);;;Argument[0];path-injection",
";FileManager;true;createFile(atPath:contents:attributes:);;;Argument[0];path-injection",
";FileManager;true;removeItem(at:);;;Argument[0];path-injection",
";FileManager;true;removeItem(atPath:);;;Argument[0];path-injection",
";FileManager;true;trashItem(at:resultingItemURL:);;;Argument[0];path-injection",
";FileManager;true;replaceItem(at:withItemAt:backupItemName:options:resultingItemURL:);;;Argument[0..1];path-injection",
";FileManager;true;replaceItemAt(_:withItemAt:backupItemName:options:);;;Argument[0..1];path-injection",
";FileManager;true;copyItem(at:to:);;;Argument[0..1];path-injection",
";FileManager;true;copyItem(atPath:toPath:);;;Argument[0..1];path-injection",
";FileManager;true;moveItem(at:to:);;;Argument[0..1];path-injection",
";FileManager;true;moveItem(atPath:toPath:);;;Argument[0..1];path-injection",
";FileManager;true;createSymbolicLink(at:withDestinationURL:);;;Argument[0..1];path-injection",
";FileManager;true;createSymbolicLink(atPath:withDestinationPath:);;;Argument[0..1];path-injection",
";FileManager;true;linkItem(at:to:);;;Argument[0..1];path-injection",
";FileManager;true;linkItem(atPath:toPath:);;;Argument[0..1];path-injection",
";FileManager;true;destinationOfSymbolicLink(atPath:);;;Argument[0];path-injection",
";FileManager;true;fileExists(atPath:);;;Argument[0];path-injection",
";FileManager;true;fileExists(atPath:isDirectory:);;;Argument[0];path-injection",
";FileManager;true;setAttributes(_:ofItemAtPath:);;;Argument[1];path-injection",
";FileManager;true;contents(atPath:);;;Argument[0];path-injection",
";FileManager;true;contentsEqual(atPath:andPath:);;;Argument[0..1];path-injection",
";FileManager;true;changeCurrentDirectoryPath(_:);;;Argument[0];path-injection",
";FileManager;true;unmountVolume(at:options:completionHandler:);;;Argument[0];path-injection",
// Deprecated FileManager methods:
";FileManager;true;changeFileAttributes(_:atPath:);;;Argument[1];path-injection",
";FileManager;true;directoryContents(atPath:);;;Argument[0];path-injection",
";FileManager;true;createDirectory(atPath:attributes:);;;Argument[0];path-injection",
";FileManager;true;createSymbolicLink(atPath:pathContent:);;;Argument[0..1];path-injection",
";FileManager;true;pathContentOfSymbolicLink(atPath:);;;Argument[0];path-injection",
";FileManager;true;replaceItemAtURL(originalItemURL:withItemAtURL:backupItemName:options:);;;Argument[0..1];path-injection",
";NIOFileHandle;true;init(descriptor:);;;Argument[0];path-injection",
";NIOFileHandle;true;init(path:mode:flags:);;;Argument[0];path-injection",
";NIOFileHandle;true;init(path:);;;Argument[0];path-injection"
]
}
}

View File

@@ -0,0 +1,30 @@
/**
* Provides a taint-tracking configuration for reasoning about path injection
* vulnerabilities.
*/
import swift
private import codeql.swift.dataflow.DataFlow
private import codeql.swift.dataflow.ExternalFlow
private import codeql.swift.dataflow.FlowSources
private import codeql.swift.dataflow.TaintTracking
private import codeql.swift.security.PathInjection
/**
* A taint-tracking configuration for path injection vulnerabilities.
*/
class PathInjectionConfiguration extends TaintTracking::Configuration {
PathInjectionConfiguration() { this = "PathInjectionConfiguration" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof PathInjectionSink }
override predicate isSanitizer(DataFlow::Node sanitizer) {
sanitizer instanceof PathInjectionSanitizer
}
override predicate isAdditionalTaintStep(DataFlow::Node node1, DataFlow::Node node2) {
any(PathInjectionAdditionalTaintStep s).step(node1, node2)
}
}

View File

@@ -0,0 +1,58 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>Accessing paths controlled by users can expose resources to attackers.</p>
<p>Paths that are naively constructed from data controlled by a user may contain unexpected special characters,
such as <code>..</code>. Such a path could point to any directory on the file system.</p>
</overview>
<recommendation>
<p>Validate user input before using it to construct a file path. Ideally, follow these rules:</p>
<ul>
<li>Do not allow more than a single <code>.</code> character.</li>
<li>Do not allow directory separators such as <code>/</code> or <code>\</code> (depending on the file system).</li>
<li>Do not rely on simply replacing problematic sequences such as <code>../</code>. For example, after applying this filter to
<code>.../...//</code> the resulting string would still be <code>../</code>.</li>
<li>Use a whitelist of known good patterns.</li>
</ul>
</recommendation>
<example>
<p>
The following code shows two bad examples.
</p>
<sample src="PathInjectionBad.swift" />
<p>
In the first, a file name is read from an HTTP request and then used to access a file. In this case, a malicious response could include a file name that is an absolute path, such as
<code>"/Applications/(current_application)/Documents/sensitive.data"</code>.
</p>
<p>
In the second bad example, it appears that the user is restricted to opening a file within the
<code>"/Library/Caches"</code> home directory. In this case, a malicious response could contain a file name containing
special characters. For example, the string <code>"../../Documents/sensitive.data"</code> will result in the code
reading the file located at <code>"/Applications/(current_application)/Library/Caches/../../Documents/sensitive.data"</code>,
which contains users' sensitive data. This file may then be made accessible to an attacker, giving them access to all this data.
</p>
<p>
In the following (good) example, the path used to access the file system is normalized <em>before</em> being checked against a
known prefix. This ensures that regardless of the user input, the resulting path is safe.
</p>
<sample src="PathInjectionGood.swift" />
</example>
<references>
<li>OWASP: <a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,25 @@
/**
* @name Uncontrolled data used in path expression
* @description Accessing paths influenced by users can allow an attacker to access unexpected resources.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id swift/path-injection
* @tags security
* external/cwe/cwe-022
* external/cwe/cwe-023
* external/cwe/cwe-036
* external/cwe/cwe-073
* external/cwe/cwe-099
*/
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.security.PathInjectionQuery
import DataFlow::PathGraph
from DataFlow::PathNode source, DataFlow::PathNode sink
where any(PathInjectionConfiguration c).hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This path depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,10 @@
let fm = FileManager.default
let path = try String(contentsOf: URL(string: "http://example.com/")!)
// BAD
return fm.contents(atPath: path)
// BAD
if (path.hasPrefix(NSHomeDirectory() + "/Library/Caches")) {
return fm.contents(atPath: path)
}

View File

@@ -0,0 +1,8 @@
let fm = FileManager.default
let path = try String(contentsOf: URL(string: "http://example.com/")!)
// GOOD
let filePath = FilePath(stringLiteral: path)
if (filePath.lexicallyNormalized().starts(with: FilePath(stringLiteral: NSHomeDirectory() + "/Library/Caches"))) {
return fm.contents(atPath: path)
}

View File

@@ -0,0 +1,24 @@
import swift
import codeql.swift.dataflow.DataFlow
import codeql.swift.dataflow.FlowSources
import codeql.swift.security.PathInjectionQuery
import TestUtilities.InlineExpectationsTest
class PathInjectionTest extends InlineExpectationsTest {
PathInjectionTest() { this = "PathInjectionTest" }
override string getARelevantTag() { result = "hasPathInjection" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(
PathInjectionConfiguration config, DataFlow::Node source, DataFlow::Node sink, Expr sinkExpr
|
config.hasFlow(source, sink) and
sinkExpr = sink.asExpr() and
location = sinkExpr.getLocation() and
element = sinkExpr.toString() and
tag = "hasPathInjection" and
value = source.asExpr().getLocation().getStartLine().toString()
)
}
}

View File

@@ -0,0 +1,174 @@
// --- stubs ---
struct URL {
init?(string: String) {}
}
class NSURL {
init?(string: String) {}
}
extension String {
init(contentsOf: URL) {
let data = ""
self.init(data)
}
}
class Data {
struct WritingOptions : OptionSet { let rawValue: Int }
init<S>(_ elements: S) {}
func write(to: URL, options: Data.WritingOptions = []) {}
}
class NSData {
struct WritingOptions : OptionSet { let rawValue: Int }
func write(to: URL, atomically: Bool) -> Bool { return false }
func write(to: URL, options: NSData.WritingOptions) {}
func write(toFile: String, atomically: Bool) -> Bool { return false }
func write(toFile: String, options: NSData.WritingOptions) {}
}
struct URLResourceKey {}
struct FileAttributeKey : Hashable {}
struct ObjCBool {}
struct AutoreleasingUnsafeMutablePointer<Pointee> {}
struct FilePath {
init(stringLiteral: String) {}
func lexicallyNormalized() -> FilePath { return FilePath(stringLiteral: "") }
func starts(with: FilePath) -> Bool { return false }
}
class FileManager {
class DirectoryEnumerator {}
struct DirectoryEnumerationOptions : OptionSet { let rawValue: Int }
struct ItemReplacementOptions : OptionSet { let rawValue: Int }
struct UnmountOptions : OptionSet { let rawValue: Int }
struct SearchPathDomainMask {}
enum SearchPathDirectory : UInt { case none }
enum URLRelationship : Int { case none }
func contentsOfDirectory(at: URL, includingPropertiesForKeys: [URLResourceKey]?, options: FileManager.DirectoryEnumerationOptions) -> [URL] { return []}
func contentsOfDirectory(atPath: String) -> [String] { return [] }
func enumerator(at: URL, includingPropertiesForKeys: [URLResourceKey]?, options: FileManager.DirectoryEnumerationOptions, errorHandler: ((URL, Error) -> Bool)?) -> FileManager.DirectoryEnumerator? { return nil }
func enumerator(atPath: String) -> FileManager.DirectoryEnumerator? { return nil }
func subpathsOfDirectory(atPath: String) -> [String] { return [] }
func subpaths(atPath: String) -> [String]? { return nil }
func createDirectory(at: URL, withIntermediateDirectories: Bool, attributes: [FileAttributeKey : Any]?) {}
func createDirectory(atPath: String, withIntermediateDirectories: Bool, attributes: [FileAttributeKey : Any]?) {}
func createFile(atPath: String, contents: Data?, attributes: [FileAttributeKey : Any]?) -> Bool { return false }
func removeItem(at: URL) {}
func removeItem(atPath: String) {}
func trashItem(at: URL, resultingItemURL: AutoreleasingUnsafeMutablePointer<NSURL?>?) {}
func replaceItemAt(_: URL, withItemAt: URL, backupItemName: String?, options: FileManager.ItemReplacementOptions) -> URL? { return nil}
func replaceItem(at: URL, withItemAt: URL, backupItemName: String?, options: FileManager.ItemReplacementOptions, resultingItemURL: AutoreleasingUnsafeMutablePointer<NSURL?>?) {}
func copyItem(at: URL, to: URL) {}
func copyItem(atPath: String, toPath: String) {}
func moveItem(at: URL, to: URL) {}
func moveItem(atPath: String, toPath: String) {}
func createSymbolicLink(at: URL, withDestinationURL: URL) {}
func createSymbolicLink(atPath: String, withDestinationPath: String) {}
func linkItem(at: URL, to: URL) {}
func linkItem(atPath: String, toPath: String) {}
func destinationOfSymbolicLink(atPath: String) -> String { return "" }
func fileExists(atPath: String) -> Bool { return false }
func fileExists(atPath: String, isDirectory: UnsafeMutablePointer<ObjCBool>?) -> Bool { return false }
func setAttributes(_: [FileAttributeKey : Any], ofItemAtPath: String) {}
func contents(atPath: String) -> Data? { return nil }
func contentsEqual(atPath: String, andPath: String) -> Bool { return false }
func changeCurrentDirectoryPath(_: String) -> Bool { return false }
func unmountVolume(at: URL, options: FileManager.UnmountOptions, completionHandler: (Error?) -> Void) {}
// Deprecated methods
func changeFileAttributes(_: [AnyHashable : Any], atPath: String) -> Bool { return false }
func directoryContents(atPath: String) -> [Any]? { return nil }
func createDirectory(atPath: String, attributes: [AnyHashable : Any]) -> Bool { return false }
func createSymbolicLink(atPath: String, pathContent: String) -> Bool { return false }
func pathContentOfSymbolicLink(atPath: String) -> String? { return nil }
func replaceItemAtURL(originalItemURL: NSURL, withItemAtURL: NSURL, backupItemName: String?, options: FileManager.ItemReplacementOptions) -> NSURL? { return nil }
}
// --- tests ---
func test() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let remoteUrl = URL(string: remoteString)!
let remoteNsUrl = NSURL(string: remoteString)!
let safeUrl = URL(string: "")!
let safeNsUrl = NSURL(string: "")!
Data("").write(to: remoteUrl, options: []) // $ hasPathInjection=97
let nsData = NSData()
let _ = nsData.write(to: remoteUrl, atomically: false) // $ hasPathInjection=97
nsData.write(to: remoteUrl, options: []) // $ hasPathInjection=97
let _ = nsData.write(toFile: remoteString, atomically: false) // $ hasPathInjection=97
nsData.write(toFile: remoteString, options: []) // $ hasPathInjection=97
let fm = FileManager()
let _ = fm.contentsOfDirectory(at: remoteUrl, includingPropertiesForKeys: [], options: []) // $ hasPathInjection=97
let _ = fm.contentsOfDirectory(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.enumerator(at: remoteUrl, includingPropertiesForKeys: [], options: [], errorHandler: nil) // $ hasPathInjection=97
let _ = fm.enumerator(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.subpathsOfDirectory(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.subpaths(atPath: remoteString) // $ hasPathInjection=97
fm.createDirectory(at: remoteUrl, withIntermediateDirectories: false, attributes: [:]) // $ hasPathInjection=97
let _ = fm.createDirectory(atPath: remoteString, attributes: [:]) // $ hasPathInjection=97
let _ = fm.createFile(atPath: remoteString, contents: nil, attributes: [:]) // $ hasPathInjection=97
fm.removeItem(at: remoteUrl) // $ hasPathInjection=97
fm.removeItem(atPath: remoteString) // $ hasPathInjection=97
fm.trashItem(at: remoteUrl, resultingItemURL: AutoreleasingUnsafeMutablePointer<NSURL?>()) // $ hasPathInjection=97
let _ = fm.replaceItemAt(remoteUrl, withItemAt: safeUrl, backupItemName: nil, options: []) // $ hasPathInjection=97
let _ = fm.replaceItemAt(safeUrl, withItemAt: remoteUrl, backupItemName: nil, options: []) // $ hasPathInjection=97
fm.replaceItem(at: remoteUrl, withItemAt: safeUrl, backupItemName: nil, options: [], resultingItemURL: AutoreleasingUnsafeMutablePointer<NSURL?>()) // $ hasPathInjection=97
fm.replaceItem(at: safeUrl, withItemAt: remoteUrl, backupItemName: nil, options: [], resultingItemURL: AutoreleasingUnsafeMutablePointer<NSURL?>()) // $ hasPathInjection=97
fm.copyItem(at: remoteUrl, to: safeUrl) // $ hasPathInjection=97
fm.copyItem(at: safeUrl, to: remoteUrl) // $ hasPathInjection=97
fm.copyItem(atPath: remoteString, toPath: "") // $ hasPathInjection=97
fm.copyItem(atPath: "", toPath: remoteString) // $ hasPathInjection=97
fm.moveItem(at: remoteUrl, to: safeUrl) // $ hasPathInjection=97
fm.moveItem(at: safeUrl, to: remoteUrl) // $ hasPathInjection=97
fm.moveItem(atPath: remoteString, toPath: "") // $ hasPathInjection=97
fm.moveItem(atPath: "", toPath: remoteString) // $ hasPathInjection=97
fm.createSymbolicLink(at: remoteUrl, withDestinationURL: safeUrl) // $ hasPathInjection=97
fm.createSymbolicLink(at: safeUrl, withDestinationURL: remoteUrl) // $ hasPathInjection=97
fm.createSymbolicLink(atPath: remoteString, withDestinationPath: "") // $ hasPathInjection=97
fm.createSymbolicLink(atPath: "", withDestinationPath: remoteString) // $ hasPathInjection=97
fm.linkItem(at: remoteUrl, to: safeUrl) // $ hasPathInjection=97
fm.linkItem(at: safeUrl, to: remoteUrl) // $ hasPathInjection=97
fm.linkItem(atPath: remoteString, toPath: "") // $ hasPathInjection=97
fm.linkItem(atPath: "", toPath: remoteString) // $ hasPathInjection=97
let _ = fm.destinationOfSymbolicLink(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.fileExists(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.fileExists(atPath: remoteString, isDirectory: UnsafeMutablePointer<ObjCBool>.init(bitPattern: 0)) // $ hasPathInjection=97
fm.setAttributes([:], ofItemAtPath: remoteString) // $ hasPathInjection=97
let _ = fm.contents(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.contentsEqual(atPath: remoteString, andPath: "") // $ hasPathInjection=97
let _ = fm.contentsEqual(atPath: "", andPath: remoteString) // $ hasPathInjection=97
let _ = fm.changeCurrentDirectoryPath(remoteString) // $ hasPathInjection=97
let _ = fm.unmountVolume(at: remoteUrl, options: [], completionHandler: { _ in }) // $ hasPathInjection=97
// Deprecated methods
let _ = fm.changeFileAttributes([:], atPath: remoteString) // $ hasPathInjection=97
let _ = fm.directoryContents(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.createDirectory(atPath: remoteString, attributes: [:]) // $ hasPathInjection=97
let _ = fm.createSymbolicLink(atPath: remoteString, pathContent: "") // $ hasPathInjection=97
let _ = fm.createSymbolicLink(atPath: "", pathContent: remoteString) // $ hasPathInjection=97
let _ = fm.pathContentOfSymbolicLink(atPath: remoteString) // $ hasPathInjection=97
let _ = fm.replaceItemAtURL(originalItemURL: remoteNsUrl, withItemAtURL: safeNsUrl, backupItemName: nil, options: []) // $ hasPathInjection=97
let _ = fm.replaceItemAtURL(originalItemURL: safeNsUrl, withItemAtURL: remoteNsUrl, backupItemName: nil, options: []) // $ hasPathInjection=97
}
func testSanitizers() {
let remoteString = String(contentsOf: URL(string: "http://example.com/")!)
let fm = FileManager()
let filePath = FilePath(stringLiteral: remoteString)
if (filePath.lexicallyNormalized().starts(with: FilePath(stringLiteral: "/safe"))) {
fm.contents(atPath: remoteString) // Safe
}
fm.contents(atPath: remoteString) // $ hasPathInjection=165
}