Skip to content

python-security/sast-semgrep: false-positive dynamic urllib-use findings (Bandit B310 + Semgrep dynamic-urllib-use-detected) now block every PR #2248

Description

@seonghobae

Symptom

Semgrep (multi-language SAST) fails with "Semgrep found WARNING/ERROR (Medium+) findings" on .github PR #2065 (run 35240824316, job 105268505006), against merged head f2b5ecae4865ef2ba630d15abcde007ba57bd126:

SEMGREP_FINDING rule=python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected level=warning path=scripts/ci/codeql_ghas_configuration_identity.py line=158 message=Detected a dynamic value being used with urllib. ...
SEMGREP_FINDING rule=python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected level=warning path=scripts/ci/strix_evidence_binding.py line=264 message=Detected a dynamic value being used with urllib. ...

Update: Bandit (Python SAST) (job 105335869601, same PR, same head) independently failed on the same two call sites with its own rule for the identical pattern:

B310: Audit url open for permitted schemes. Allowing use of file:/ or custom schemes is often unexpected. (severity=MEDIUM, confidence=HIGH)

(2 findings — Bandit's console output doesn't echo path/line, but scripts/ci/ has exactly two unsuppressed urlopen() call sites on main, both in the files above; see below.)

Both flagged lines are on main, unmodified by PR #2065 (whose own diff is .gitleaks.toml only) — confirmed via git grep -n "urlopen(" origin/main -- scripts/.

Why this is a false positive

  • scripts/ci/codeql_ghas_configuration_identity.py:158 — _request_json(url, ...) calls urllib.request.urlopen(request, ...) where request wraps url. Its only caller (list_codeql_analyses, line ~191) builds url as f"https://api.github.com/repos/{repository}/code-scanning/analyses?{query}" — the scheme and host are a fixed literal; only the owner/repo path segment and a urlencoded query string are dynamic.
  • scripts/ci/strix_evidence_binding.py:264 — same shape: Request(url, ...) then urlopen(request, timeout=30) # noqa: S310 - GitHub HTTPS only, with url built against the fixed https://api.github.com base.

Semgrep's dynamic-urllib-use-detected and Bandit's B310 both flag any urlopen() call whose argument isn't a string literal, regardless of whether the dynamic part can actually change the scheme/host. Neither url here is influenced by PR input, issue/comment bodies, or any other untrusted source — both are internal GitHub REST API calls with a hardcoded trusted host. Same character as the CodeQL false positive already tracked and fixed in #2208 (py/incomplete-url-substring-sanitization), just two more scanners/rules catching the same syntactic pattern in the same two files.

Notable finding: strix_evidence_binding.py:264 already carries a # noqa: S310 comment — but # noqa is the Ruff/flake8 suppression syntax, not Bandit's. Bandit only recognizes # nosec or # nosec B310. So this line already shows someone reasoned about the Bandit finding and tried to suppress it, but used the wrong directive — it has never actually silenced Bandit. scripts/ci/materialize_base_python_requirements.py:363 shows the correct dual-suppression pattern already in use elsewhere in this same repo: # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected # nosec B310.

Why it will affect every PR

Like #2208, the findings are on main and unscoped by any Semgrep/Bandit config (sast-semgrep.yml passes only --config=p/default, --exclude=.github/workflows, --exclude='docs/research/**/standards'; no path-ignore for scripts/ci). Every PR that triggers a fresh whole-tree scan will hit both until resolved on main.

Remediation options (mirroring #2208's structure)

  1. Narrow, no new mechanism: assert the resolved URL's scheme/host immediately before each urlopen/Request call, e.g. assert url.startswith("https://api.github.com/"), so the dynamic value is validated in a way both scanners' heuristics recognize. Two call sites, two files.
  2. Suppress correctly: replace strix_evidence_binding.py:264's # noqa: S310 with the same dual-suppression comment already used at materialize_base_python_requirements.py:363 (# nosemgrep: ... # nosec B310), and add the equivalent to codeql_ghas_configuration_identity.py:158 (currently unsuppressed for either tool). Fixes both scanners at once, matches existing repo convention, but doesn't remove the underlying heuristic mismatch the way option 1 does.
  3. Scope the scan to exclude scripts/ci from these rules — broadest effect, needs its own justification since it would stop scanning that directory for this whole rule class, for both scanners.

Either option 1 or 2 looks right (2 is the path of least resistance since materialize_base_python_requirements.py already establishes the convention), but I am not the writer of codeql_ghas_configuration_identity.py or strix_evidence_binding.py and my open PR (#2065) doesn't touch either file, so I'm not taking it there — filing this per the same not-bypass-eligible governance rule #2208 documented.

Notes for whoever picks this up

  • Zero open PRs currently touch either flagged file (checked via search_issues/PR search at filing time), so the lane should be free.
  • Whichever option is chosen, fix both call sites together — partial suppression (e.g. only Semgrep or only Bandit) will leave the other scanner red on the next PR.

Filed from PR #2065, whose own diff is one unrelated .gitleaks.toml allowlist entry.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: criticalImmediate blocker, P0, urgent deadlock, or critical incident

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions