From 233c3ce30d70aba4d450ee280adc95b8cfe6f64d Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 9 Jul 2026 16:47:49 +0100 Subject: [PATCH] Fix incorrect sanitizers --- ...6-07-09-remove-incorrect-path-sanitizer.md | 4 ++ go/ql/lib/ext/path.filepath.model.yml | 5 --- .../go/security/TaintedPathCustomizations.qll | 37 ------------------- .../Security/CWE-022/TaintedPath.expected | 15 +++++++- .../Security/CWE-022/TaintedPath.go | 8 ++-- .../Security/CWE-022/ZipSlip.expected | 17 ++++++++- .../test/query-tests/Security/CWE-022/tst.go | 8 ++-- 7 files changed, 40 insertions(+), 54 deletions(-) create mode 100644 go/ql/lib/change-notes/2026-07-09-remove-incorrect-path-sanitizer.md diff --git a/go/ql/lib/change-notes/2026-07-09-remove-incorrect-path-sanitizer.md b/go/ql/lib/change-notes/2026-07-09-remove-incorrect-path-sanitizer.md new file mode 100644 index 000000000000..4e7a4be2cafe --- /dev/null +++ b/go/ql/lib/change-notes/2026-07-09-remove-incorrect-path-sanitizer.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The function `Rel` in `path/filepath` was incorrectly considered a sanitizer for `go/path-injection` and `go/zipslip`. This has now been fixed, which may lead to more results for those queries. diff --git a/go/ql/lib/ext/path.filepath.model.yml b/go/ql/lib/ext/path.filepath.model.yml index d450e2bbc568..15bcb7d386d8 100644 --- a/go/ql/lib/ext/path.filepath.model.yml +++ b/go/ql/lib/ext/path.filepath.model.yml @@ -1,9 +1,4 @@ extensions: - - addsTo: - pack: codeql/go-all - extensible: barrierModel - data: - - ["path/filepath", "", False, "Rel", "", "", "ReturnValue", "path-injection", "manual"] - addsTo: pack: codeql/go-all extensible: summaryModel diff --git a/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll b/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll index 20341159c64c..d49365537373 100644 --- a/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll +++ b/go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll @@ -129,43 +129,6 @@ module TaintedPath { DotDotReplaceAll() { this.getReplacedString() = ["..", "."] } } - /** - * A node `nd` guarded by a check that ensures it is contained within some root folder, - * considered as a sanitizer for path traversal. - * - * We currently recognize checks of the following form: - * - * ``` - * ..., err := filepath.Rel(base, path) - * if err == nil { - * // path is known to be contained in base - * } - * ``` - */ - class PathContainmentCheck extends SanitizerGuard, DataFlow::EqualityTestNode { - DataFlow::Node path; - boolean outcome; - - PathContainmentCheck() { - exists(Function f, FunctionInput inp, FunctionOutput outp, DataFlow::Property p | - f.hasQualifiedName("path/filepath", "Rel") and - inp.isParameter(1) and - outp.isResult(1) and - p.isNil() - | - exists(DataFlow::Node call, DataFlow::Node res | - call = f.getACall() and - DataFlow::localFlow(outp.getNode(call), res) - | - p.checkOn(this, outcome, res) and - path = inp.getNode(call) - ) - ) - } - - override predicate checks(Expr e, boolean branch) { e = path.asExpr() and branch = outcome } - } - /** * A call of the form `strings.HasPrefix(path, ...)` considered as a sanitizer guard * for `path`. diff --git a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.expected b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.expected index c95fa5e7af62..851ed3533ae9 100644 --- a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.expected +++ b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.expected @@ -1,25 +1,36 @@ #select | TaintedPath.go:18:29:18:40 | tainted_path | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:18:29:18:40 | tainted_path | This path depends on a $@. | TaintedPath.go:15:18:15:22 | selection of URL | user-provided value | | TaintedPath.go:22:28:22:69 | call to Join | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:22:28:22:69 | call to Join | This path depends on a $@. | TaintedPath.go:15:18:15:22 | selection of URL | user-provided value | +| TaintedPath.go:27:28:27:45 | sanitized_filepath | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:27:28:27:45 | sanitized_filepath | This path depends on a $@. | TaintedPath.go:15:18:15:22 | selection of URL | user-provided value | +| TaintedPath.go:43:29:43:40 | tainted_path | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:43:29:43:40 | tainted_path | This path depends on a $@. | TaintedPath.go:15:18:15:22 | selection of URL | user-provided value | | TaintedPath.go:74:28:74:57 | call to Clean | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:74:28:74:57 | call to Clean | This path depends on a $@. | TaintedPath.go:15:18:15:22 | selection of URL | user-provided value | edges | TaintedPath.go:15:18:15:22 | selection of URL | TaintedPath.go:15:18:15:30 | call to Query | provenance | Src:MaD:2 MaD:3 | | TaintedPath.go:15:18:15:30 | call to Query | TaintedPath.go:18:29:18:40 | tainted_path | provenance | Sink:MaD:1 | | TaintedPath.go:15:18:15:30 | call to Query | TaintedPath.go:22:57:22:68 | tainted_path | provenance | | | TaintedPath.go:22:57:22:68 | tainted_path | TaintedPath.go:22:28:22:69 | call to Join | provenance | FunctionModel Sink:MaD:1 | +| TaintedPath.go:22:57:22:68 | tainted_path | TaintedPath.go:26:63:26:74 | tainted_path | provenance | | +| TaintedPath.go:22:57:22:68 | tainted_path | TaintedPath.go:43:29:43:40 | tainted_path | provenance | Sink:MaD:1 | | TaintedPath.go:22:57:22:68 | tainted_path | TaintedPath.go:74:39:74:56 | ...+... | provenance | | -| TaintedPath.go:74:39:74:56 | ...+... | TaintedPath.go:74:28:74:57 | call to Clean | provenance | MaD:4 Sink:MaD:1 | +| TaintedPath.go:26:2:26:75 | ... := ...[0] | TaintedPath.go:27:28:27:45 | sanitized_filepath | provenance | Sink:MaD:1 | +| TaintedPath.go:26:63:26:74 | tainted_path | TaintedPath.go:26:2:26:75 | ... := ...[0] | provenance | MaD:4 | +| TaintedPath.go:74:39:74:56 | ...+... | TaintedPath.go:74:28:74:57 | call to Clean | provenance | MaD:5 Sink:MaD:1 | models | 1 | Sink: io/ioutil; ; false; ReadFile; ; ; Argument[0]; path-injection; manual | | 2 | Source: net/http; Request; true; URL; ; ; ; remote; manual | | 3 | Summary: net/url; URL; true; Query; ; ; Argument[receiver]; ReturnValue; taint; manual | -| 4 | Summary: path; ; false; Clean; ; ; Argument[0]; ReturnValue; taint; manual | +| 4 | Summary: path/filepath; ; false; Rel; ; ; Argument[0..1]; ReturnValue[0]; taint; manual | +| 5 | Summary: path; ; false; Clean; ; ; Argument[0]; ReturnValue; taint; manual | nodes | TaintedPath.go:15:18:15:22 | selection of URL | semmle.label | selection of URL | | TaintedPath.go:15:18:15:30 | call to Query | semmle.label | call to Query | | TaintedPath.go:18:29:18:40 | tainted_path | semmle.label | tainted_path | | TaintedPath.go:22:28:22:69 | call to Join | semmle.label | call to Join | | TaintedPath.go:22:57:22:68 | tainted_path | semmle.label | tainted_path | +| TaintedPath.go:26:2:26:75 | ... := ...[0] | semmle.label | ... := ...[0] | +| TaintedPath.go:26:63:26:74 | tainted_path | semmle.label | tainted_path | +| TaintedPath.go:27:28:27:45 | sanitized_filepath | semmle.label | sanitized_filepath | +| TaintedPath.go:43:29:43:40 | tainted_path | semmle.label | tainted_path | | TaintedPath.go:74:28:74:57 | call to Clean | semmle.label | call to Clean | | TaintedPath.go:74:39:74:56 | ...+... | semmle.label | ...+... | subpaths diff --git a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go index 812b56f7c94b..680c108d0562 100644 --- a/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go +++ b/go/ql/test/query-tests/Security/CWE-022/TaintedPath.go @@ -22,9 +22,9 @@ func handler(w http.ResponseWriter, r *http.Request) { data, _ = ioutil.ReadFile(filepath.Join("/home/user/", tainted_path)) // $ Alert[go/path-injection] w.Write(data) - // GOOD: This can only read inside the provided safe path + // BAD: This could still read any file on the file system sanitized_filepath, _ := filepath.Rel("/home/user/safepath", tainted_path) - data, _ = ioutil.ReadFile(sanitized_filepath) + data, _ = ioutil.ReadFile(sanitized_filepath) // $ Alert[go/path-injection] w.Write(data) // GOOD: This can only read inside the provided safe path @@ -37,10 +37,10 @@ func handler(w http.ResponseWriter, r *http.Request) { data, _ = ioutil.ReadFile(strings.ReplaceAll(tainted_path, "..", "")) w.Write(data) - // GOOD: This can only read inside the provided safe path + // BAD: This could still read any file on the file system _, err := filepath.Rel("/home/user/safepath", tainted_path) if err == nil { - data, _ = ioutil.ReadFile(tainted_path) + data, _ = ioutil.ReadFile(tainted_path) // $ Alert[go/path-injection] w.Write(data) } diff --git a/go/ql/test/query-tests/Security/CWE-022/ZipSlip.expected b/go/ql/test/query-tests/Security/CWE-022/ZipSlip.expected index 3bfd80a120ca..88f1666af3b9 100644 --- a/go/ql/test/query-tests/Security/CWE-022/ZipSlip.expected +++ b/go/ql/test/query-tests/Security/CWE-022/ZipSlip.expected @@ -2,7 +2,9 @@ | UnsafeUnzipSymlinkGood.go:72:3:72:25 | ... := ...[0] | UnsafeUnzipSymlinkGood.go:72:3:72:25 | ... := ...[0] | UnsafeUnzipSymlinkGood.go:61:31:61:62 | call to Join | Unsanitized archive entry, which may contain '..', is used in a $@. | UnsafeUnzipSymlinkGood.go:61:31:61:62 | call to Join | file system operation | | ZipSlip.go:11:2:15:2 | range statement[1] | ZipSlip.go:11:2:15:2 | range statement[1] | ZipSlip.go:14:20:14:20 | p | Unsanitized archive entry, which may contain '..', is used in a $@. | ZipSlip.go:14:20:14:20 | p | file system operation | | tarslip.go:15:2:15:30 | ... := ...[0] | tarslip.go:15:2:15:30 | ... := ...[0] | tarslip.go:16:14:16:34 | call to Dir | Unsanitized archive entry, which may contain '..', is used in a $@. | tarslip.go:16:14:16:34 | call to Dir | file system operation | +| tst.go:23:2:43:2 | range statement[1] | tst.go:23:2:43:2 | range statement[1] | tst.go:27:21:27:48 | call to Join | Unsanitized archive entry, which may contain '..', is used in a $@. | tst.go:27:21:27:48 | call to Join | file system operation | | tst.go:23:2:43:2 | range statement[1] | tst.go:23:2:43:2 | range statement[1] | tst.go:29:20:29:23 | path | Unsanitized archive entry, which may contain '..', is used in a $@. | tst.go:29:20:29:23 | path | file system operation | +| tst.go:23:2:43:2 | range statement[1] | tst.go:23:2:43:2 | range statement[1] | tst.go:31:21:31:24 | path | Unsanitized archive entry, which may contain '..', is used in a $@. | tst.go:31:21:31:24 | path | file system operation | edges | UnsafeUnzipSymlinkGood.go:52:24:52:32 | SSA def(candidate) | UnsafeUnzipSymlinkGood.go:61:53:61:61 | candidate | provenance | | | UnsafeUnzipSymlinkGood.go:61:53:61:61 | candidate | UnsafeUnzipSymlinkGood.go:61:31:61:62 | call to Join | provenance | FunctionModel Sink:MaD:3 | @@ -14,14 +16,20 @@ edges | ZipSlip.go:12:3:12:30 | ... := ...[0] | ZipSlip.go:14:20:14:20 | p | provenance | Sink:MaD:1 | | ZipSlip.go:12:24:12:29 | selection of Name | ZipSlip.go:12:3:12:30 | ... := ...[0] | provenance | MaD:4 | | tarslip.go:15:2:15:30 | ... := ...[0] | tarslip.go:16:23:16:33 | selection of Name | provenance | | -| tarslip.go:16:23:16:33 | selection of Name | tarslip.go:16:14:16:34 | call to Dir | provenance | MaD:5 Sink:MaD:2 | +| tarslip.go:16:23:16:33 | selection of Name | tarslip.go:16:14:16:34 | call to Dir | provenance | MaD:6 Sink:MaD:2 | +| tst.go:23:2:43:2 | range statement[1] | tst.go:25:38:25:41 | path | provenance | | | tst.go:23:2:43:2 | range statement[1] | tst.go:29:20:29:23 | path | provenance | Sink:MaD:1 | +| tst.go:23:2:43:2 | range statement[1] | tst.go:31:21:31:24 | path | provenance | Sink:MaD:1 | +| tst.go:25:3:25:42 | ... := ...[0] | tst.go:27:41:27:47 | relpath | provenance | | +| tst.go:25:38:25:41 | path | tst.go:25:3:25:42 | ... := ...[0] | provenance | MaD:5 | +| tst.go:27:41:27:47 | relpath | tst.go:27:21:27:48 | call to Join | provenance | FunctionModel Sink:MaD:1 | models | 1 | Sink: io/ioutil; ; false; WriteFile; ; ; Argument[0]; path-injection; manual | | 2 | Sink: os; ; false; MkdirAll; ; ; Argument[0]; path-injection; manual | | 3 | Sink: os; ; false; Readlink; ; ; Argument[0]; path-injection; manual | | 4 | Summary: path/filepath; ; false; Abs; ; ; Argument[0]; ReturnValue[0]; taint; manual | -| 5 | Summary: path; ; false; Dir; ; ; Argument[0]; ReturnValue; taint; manual | +| 5 | Summary: path/filepath; ; false; Rel; ; ; Argument[0..1]; ReturnValue[0]; taint; manual | +| 6 | Summary: path; ; false; Dir; ; ; Argument[0]; ReturnValue; taint; manual | nodes | UnsafeUnzipSymlinkGood.go:52:24:52:32 | SSA def(candidate) | semmle.label | SSA def(candidate) | | UnsafeUnzipSymlinkGood.go:61:31:61:62 | call to Join | semmle.label | call to Join | @@ -37,5 +45,10 @@ nodes | tarslip.go:16:14:16:34 | call to Dir | semmle.label | call to Dir | | tarslip.go:16:23:16:33 | selection of Name | semmle.label | selection of Name | | tst.go:23:2:43:2 | range statement[1] | semmle.label | range statement[1] | +| tst.go:25:3:25:42 | ... := ...[0] | semmle.label | ... := ...[0] | +| tst.go:25:38:25:41 | path | semmle.label | path | +| tst.go:27:21:27:48 | call to Join | semmle.label | call to Join | +| tst.go:27:41:27:47 | relpath | semmle.label | relpath | | tst.go:29:20:29:23 | path | semmle.label | path | +| tst.go:31:21:31:24 | path | semmle.label | path | subpaths diff --git a/go/ql/test/query-tests/Security/CWE-022/tst.go b/go/ql/test/query-tests/Security/CWE-022/tst.go index 4cf3a77c4c8d..33b2aa072c0d 100644 --- a/go/ql/test/query-tests/Security/CWE-022/tst.go +++ b/go/ql/test/query-tests/Security/CWE-022/tst.go @@ -14,7 +14,7 @@ func uploadFile(w http.ResponseWriter, r *http.Request) { file, handler, _ := r.FormFile("file") // err handling defer file.Close() - tempFile, _ := ioutil.TempFile("/tmp", handler.Filename) // NOT OK + tempFile, _ := ioutil.TempFile("/tmp", handler.Filename) // OK use(tempFile) } @@ -24,11 +24,11 @@ func unzip2(f string, root string) { path := f.Name relpath, err := filepath.Rel(root, path) if err == nil { - ioutil.WriteFile(filepath.Join(root, relpath), []byte("present"), 0666) // OK + ioutil.WriteFile(filepath.Join(root, relpath), []byte("present"), 0666) // $ Sink[go/zipslip] } - ioutil.WriteFile(path, []byte("present"), 0666) // $ Sink[go/zipslip] // NOT OK + ioutil.WriteFile(path, []byte("present"), 0666) // $ Sink[go/zipslip] if containedIn(path, root) { - ioutil.WriteFile(path, []byte("present"), 0666) // OK + ioutil.WriteFile(path, []byte("present"), 0666) // $ Sink[go/zipslip] } if ok, _ := regexp.MatchString("^[a-z]*$", path); ok { ioutil.WriteFile(path, []byte("present"), 0666) // OK