Go: filepath.Clean with a prepended path separator is modeled as a path-injection sanitizer and causes false negatives
まだ誰も着手していません。
評価
- 難易度
- 3/5
- 見積もり時間
- 1〜2日
- 初心者へのやさしさ
- 68/100
調査の方向性
go/ql/test/query-tests/Security/CWE-022/TaintedPath.qlref を実行し、go/ql/lib/semmle/go/security/TaintedPathCustomizations.qll の FilepathCleanSanitizer を TaintedPath.go の期待される結果と併せて調査します。64 行目と 69 行目にある 2 つの filepath.Clean 呼び出しが taint を保持し、ioutil.ReadFile に到達するアラートを報告することを確認してから、テストを再実行して期待される結果を確認します。
索引モデルが issue の本文から書いたものです。
説明
Description of the issue
The Go go/path-injection query treats the result of
filepath.Clean(constantPrefix + userInput) as sanitized whenever the first
character of constantPrefix is / or \. This is not sufficient to make a
path safe. filepath.Clean normalizes a path, but it does not ensure that the
result is relative or contained within a trusted directory.
As a result, CodeQL suppresses path-injection alerts when attacker-controlled
input is converted into an attacker-controlled absolute path and passed to a
file-system sink. Two instances of this pattern are currently marked as good in
CodeQL's own TaintedPath.go test.
Affected sanitizer
FilepathCleanSanitizer in TaintedPathCustomizations.qll:
/**
* A call to `filepath.Clean("/" + e)`, considered to sanitize `e` against path traversal.
*/
class FilepathCleanSanitizer extends Sanitizer {
FilepathCleanSanitizer() {
exists(DataFlow::CallNode cleanCall, StringOps::Concatenation concatNode |
cleanCall = any(Function f | f.hasQualifiedName("path/filepath", "Clean")).getACall() and
concatNode = cleanCall.getArgument(0) and
concatNode.getOperand(0).getStringValue().prefix(1) = ["/", "\\"] and
this = cleanCall.getResult()
)
}
}
The sanitizer stops path-injection taint at the result of filepath.Clean
without checking whether the result is relative or remains under a safe root.
Why the result is still unsafe
For example, consider the first existing test case:
taintedPath := "../etc/passwd"
cleaned := filepath.Clean("/" + taintedPath)
fmt.Println(cleaned) // /etc/passwd
Prepending / prevents .. from moving above the file-system root, but the
result is still an absolute path controlled by the attacker. An attacker does
not need traversal components at all:
taintedPath := "/etc/passwd"
cleaned := filepath.Clean("/" + taintedPath)
fmt.Println(cleaned) // /etc/passwd
The broader sanitizer condition is also unsafe when the constant prefix names
a directory:
taintedPath := "/../../etc/passwd"
cleaned := filepath.Clean("/hardcoded" + taintedPath)
fmt.Println(cleaned) // /etc/passwd
filepath.Clean only performs lexical normalization. It does not establish a
containment property.
This also conflicts with the query help for go/path-injection, which states
that attacker-controlled paths may be absolute and recommends checking that a
normalized path is relative or contained within a safe directory:
TaintedPath.qhelp: absolute paths can point anywhere on the file systemTaintedPath.qhelp: normalized paths should be relative or contained within a safe folder
Existing false-negative test cases
The existing TaintedPath.go test
marks both of these sinks as good:
// GOOD: Sanitized by filepath.Clean with a prepended '/' forcing interpretation
// as an absolute path, so that Clean will throw away any leading `..` components.
data, _ = ioutil.ReadFile(filepath.Clean("/" + tainted_path))
w.Write(data)
// GOOD: Sanitized by filepath.Clean with a prepended os.PathSeparator forcing interpretation
// as an absolute path, so that Clean will throw away any leading `..` components.
data, _ = ioutil.ReadFile(filepath.Clean(string(os.PathSeparator) + "hardcoded" + tainted_path))
w.Write(data)
Both calls can resolve to /etc/passwd using the inputs shown above. The source
is r.URL.Query()["path"][0], and ioutil.ReadFile is a path-injection sink,
but no alerts are produced while FilepathCleanSanitizer is enabled.
Steps to reproduce
-
Run the existing test:
codeql test run go/ql/test/query-tests/Security/CWE-022/TaintedPath.qlrefThe test passes and no alerts are reported for
TaintedPath.golines 64 and
69. -
Remove only the
FilepathCleanSanitizerclass from
go/ql/lib/semmle/go/security/TaintedPathCustomizations.qlland rerun the
same test. -
The test fails with these two new results:
| TaintedPath.go:64:28:64:61 | call to Clean | ... | user-provided value | | TaintedPath.go:69:28:69:96 | call to Clean | ... | user-provided value | | TaintedPath.go:64:28:64:61 | call to Clean | Unexpected result: Alert | | TaintedPath.go:69:28:69:96 | call to Clean | Unexpected result: Alert |
The individual ablation changes no other alert locations in this test. This
confirms that FilepathCleanSanitizer is the component suppressing the two
source-to-sink paths.
Expected behavior
The result of filepath.Clean should not be treated as an unconditional
path-injection sanitizer merely because its input has a prepended path
separator. The calls at TaintedPath.go lines 64 and 69 should retain taint and
produce alerts when their results reach ReadFile.
A possible fix is to remove FilepathCleanSanitizer and update the affected
test expectations. Code that separately proves that the normalized path is
relative or contained within a trusted directory can still be handled by the
corresponding validation or guard models.
Environment
- CodeQL CLI 2.25.6
- CodeQL repository test baseline:
f6f45d1536 - Present in
mainat commit42843f155e95d25e690aba9ae4620b5a5986a951 - Go 1.22.12 on Linux/amd64
- 主要言語
- CodeQL
- スター
- 10.1k
- フォーク
- 2.1k
- 平均マージ
- 2日 10時間
- マージ済み PR(30日)
- 134
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
github/codeql のほかの issue
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
-
C#: cs/simplifiable-boolean-expression false positive on Nullable<bool> compared with a literal オープン
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
-
false-positive
難易度 2/5 1〜3時間 初心者へのやさしさ 70/100
-
false-positive
難易度 3/5 1〜2日 初心者へのやさしさ 68/100
似ている issue
-
documentation help wanted
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 86/100
clerk/javascript#9852 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 62/100
AiursoftWeb/AnduinOS-2#19 ·
-
area/config comp/agent P3 sweeper:risk-security-boundary type/feature
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
NousResearch/hermes-agent#118734 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 76/100
AXERA-TECH/ax-llm#75 ·