Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions signetry_core/pipeline/findings/lang_taint.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ class SinkSpec:
remediation: str
pattern: re.Pattern[str]
requires_concat: bool = True # False for sinks where the value itself is the payload
# Optional negative guard: when this matches the sink line, the line is NOT a
# finding even though a tainted identifier appears on it. Used where taint on
# the line does not mean taint in the dangerous position (e.g. a constant URL
# host with a user-supplied query string is not SSRF).
skip_if: re.Pattern[str] | None = None
confidence: float = 0.8
severity: Severity = Severity.HIGH

Expand Down Expand Up @@ -96,6 +101,11 @@ def _idents(text: str, pat: re.Pattern[str]) -> set[str]:
re.compile(r"\bhttp\.(?:Get|Head|Post|PostForm)\s*\("
r"|\b\w*[Cc]lient\.(?:Get|Head|Post|PostForm|Do)\s*\("
r"|\bhttp\.NewRequest(?:WithContext)?\s*\("),
# A literal that already covers scheme AND host pins the
# destination, so a tainted query string on the same line is not
# SSRF. Requiring >=1 host character before the closing quote or
# path keeps `"https://" + userHost` (real SSRF) firing.
skip_if=re.compile(r'"https?://[A-Za-z0-9.-]+[/"]'),
requires_concat=False, confidence=0.8),
SinkSpec("go.taint.path_traversal", "path_traversal", "CWE-22",
"File path built from user input (taint)",
Expand Down Expand Up @@ -268,6 +278,8 @@ def scan_lang_taint(file: str, text: str) -> list[Finding]:
and _PARAM_PLACEHOLDER.search(line)
and not spec.concat.search(line)):
continue
if sink.skip_if is not None and sink.skip_if.search(line):
continue
inline_src = bool(spec.source.search(line))
uses_tainted = any(v in tainted for v in _idents(line, spec.ident))
if not (inline_src or uses_tainted):
Expand Down
19 changes: 19 additions & 0 deletions tests/test_findings_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,25 @@ def test_lang_taint_go_ssrf():
assert "ssrf" in _cats(via_client, "proxy.go")


def test_lang_taint_go_ssrf_constant_host_is_not_ssrf():
# A literal covering scheme AND host pins the destination, so a user-supplied
# query string or path suffix is not SSRF — the same distinction the Python rule
# is held to. Regression: this was a false positive against the eval corpus'
# LANG-60 safe decoy.
for src in (
'q := r.URL.Query().Get("q")\n'
'resp, _ := http.Get("https://api.example.com/search?q=" + url.QueryEscape(q))\n',
'id := r.URL.Query().Get("id")\n'
'resp, _ := http.Get("https://api.example.com/items/" + id)\n',
):
assert "ssrf" not in _cats(src, "client.go"), src

# A literal that stops at the scheme does NOT pin the host — still SSRF.
attacker_host = ('h := r.URL.Query().Get("h")\n'
'resp, _ := http.Get("https://" + h)\n')
assert "ssrf" in _cats(attacker_host, "client.go")


def test_lang_taint_go_path_traversal():
src = ('name := r.URL.Query().Get("f")\n'
'data, _ := os.ReadFile("/var/data/" + name)\n')
Expand Down
Loading