Nightly security scan triage — 2026-05-31 through 2026-06-04
Triage produced: 2026-06-04T14:51Z UTC
Triaged by: security-agent
Scan PRs covered: #3170, #3184, #3194, #3197, #3214
Canonical finding set: PR #3214 (2026-06-04) — most recent; 73 grouped findings
Summary
| Classification | Count | Notes |
|---|---|---|
| ALLOWLIST | 57 | Bandit pattern FP — allow-listed by policy |
| FIX-CODE | 5 | Requires feature-developer; blocks SRE merge |
| FIX-INFRA | 0 | No infra-only fixes needed |
| RECOMMEND-CLOSE | 4 older PRs | #3170, #3184, #3194, #3197 superseded by #3214 |
| REAL-VULN | 1 | HIGH: tmp path-traversal (dev dep chain) — BLOCKS-MERGE on #3214 |
Total grouped findings across 5 reports: 82 / 82 / 72 / 72 / 73
Net new findings in #3214 vs #3170: +1 (B110 in backend_v2/api/services/mbt_fill_engine.py)
Open type:security issues (pre-existing): 20 open issues; see "Pre-existing security issue inventory" below.
Per-PR plan
PR #3170 (2026-05-31) — 82 groups
- Close-out path: CLOSE-NO-ACTION — superseded by #3214 (2026-06-04)
- Reasoning: identical finding set to #3184 (2026-06-01). All findings present in the canonical #3214 report. Merging four week-old scan docs without fixes adds noise.
- Action for SRE: close with comment: "Superseded by #3214 consolidated triage. See docs/security/remediation/2026-06-04-nightly-scans-triage.md."
PR #3184 (2026-06-01) — 82 groups
- Close-out path: CLOSE-NO-ACTION — superseded by #3214 (2026-06-04)
- Reasoning: identical finding set to #3170. Npm audit block changed (removed
@jest/core,@tootallnate/once,body-parser,express,qscluster; not present in later scans — likely an npm audit version change, not a real regression). All residual findings captured in #3214. - Action for SRE: close with comment matching #3170.
PR #3194 (2026-06-02) — 72 groups
- Close-out path: CLOSE-NO-ACTION — superseded by #3214 (2026-06-04)
- Reasoning: scan count dropped from 82 to 72; the npm block changed from
body-parser/express/qs/jestcluster tonext + postcss. All 72 bandit findings are identical to #3214's bandit block. Thenextandpostcssnpm findings are present and carried in #3214. - Action for SRE: close with comment matching #3170.
PR #3197 (2026-06-03) — 72 groups
- Close-out path: CLOSE-NO-ACTION — superseded by #3214 (2026-06-04)
- Reasoning: identical to #3194. No new findings vs #3214 except #3214 adds one B110 finding.
- Action for SRE: close with comment matching #3170.
PR #3214 (2026-06-04) — 73 groups — CANONICAL
This is the one to merge after fixes below are dispatched. Details per finding class:
ALLOWLIST findings (57 grouped findings — no action required)
These are all bandit pattern false-positives confirmed against source. Group them by rule for SRE's .bandit config PR (see Consolidated Patterns).
B110 (try-except-pass / try-except-continue) — 52 occurrences across 38 files
All production B110 hits are legitimate defensive-coding patterns in service layer code (vault client retries, flag poller fallbacks, customer-detail fetch with partial data). None swallow security-relevant exceptions (auth failures, permission checks). The one B112 (try-except-continue) at console/app/services/alerts_aggregator.py:393 is specifically skipping a malformed updated_at date parse inside a metrics aggregation loop — not a security concern.
- Classification: ALLOWLIST
- Severity: LOW (bandit rates MEDIUM; false-positive confirmed)
- Reasoning: B110/B112 in non-security-critical paths; no auth, crypto, or privilege operations inside the except blocks. Production code pattern is consistent and deliberate.
- Action: Add
B110,B112to.banditskips forconsole/app/services/,console/app/blueprints/,backend_v2/api/services/,backend_v2/api/routes/,console/migrations/paths. Dispatch to feature-developer as a single config PR (see FIX-CODE item 5 below).
B608 — backend_v2/alembic/versions/0026_raptor_app_grant_catchup.py
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning: Migration uses
f"GRANT ... ON TABLE {table} TO raptor_app"where{table}iterates over_FULL_DML_TABLES,_UPDATE_TABLES, and_APPEND_ONLY_TABLES— all hardcoded Python tuples in the same file. No user input reaches the SQL template. File already carries# noqa: S608on each execute call confirming prior intentional review. Bandit's regex fires on any f-string containing SQL keywords regardless of whether the interpolated value is a constant. - Action: Already suppressed inline (
noqa: S608). Confirm.banditconfig excludes migration files from B608 check (see FIX-CODE item 5).
B608 — console/scripts/flag_reconciler_backfill.py
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning:
_TABLE = "console_flag_promotions"is a module-level string constant. The f-stringsf"SELECT id, flag_key ... FROM {_TABLE}",f"SELECT id FROM {_TABLE}",f"INSERT INTO {_TABLE}",f"UPDATE {_TABLE}"interpolate only this constant. No user or external input reaches the table name. This is a maintenance script, not a request handler. - Action: Add B608 suppression for
console/scripts/in.banditconfig.
B608 — backend_v2/api/routes/auth.py (line 1832)
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning: Dynamic
UPDATE users SET {', '.join(set_clauses)}whereset_clausesis built exclusively fromCONTACT_KEYS = ["phone", "alt_email", "mailing_address_line1", ...]— a hardcoded tuple. Thecontact_updatesdict comes from_validate_contact_info_payload()which iterates only overCONTACT_KEYSand never adds caller-supplied key names to the result dict. Column names cannot be injected. - Action: Add B608 suppression for this file/function in
.banditconfig.
B608 — backend_v2/api/routes/strategies.py (7 occurrences)
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning: Two patterns: (1)
f"SELECT {_SELECT_COLS} FROM strategies ..."where_SELECT_COLSis a module-level multiline string constant; (2)f"UPDATE strategies SET {set_clause} WHERE ..."whereset_clauseis", ".join(f"{k} = :{k}" for k in updates)andupdateskeys are drawn exclusively from_RULE_COLUMNStuple + three explicitly named fields (name,strategy_type,description,is_active). No user-supplied key names flow into the column position. - Action: Add B608 suppression for this file in
.banditconfig.
B608 — console/app/blueprints/api_rbac_grants.py and console/app/services/rbac_grants.py
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning: RBAC grant service uses
db.text("SELECT 1 FROM rbac_groups WHERE id = :gid AND name = :name LIMIT 1")— fully parameterized. The B608 fires on string literal containing SQL keywords, not on actual interpolation. No user input in column/table position. - Action: Add B608 suppression for these files in
.banditconfig.
B105 — hardcoded password strings (6 occurrences)
Affected files: backend_v2/api/__init__.py, console/app/__init__.py, console/app/blueprints/auth.py, console/app/blueprints/deploy_freeze.py, console/app/blueprints/flags.py, console/app/blueprints/heroku_log_drain.py, console/app/services/deploy_kv.py, console/app/services/freescout_client.py, console/app/services/rotation_mode_a.py.
All confirmed as intentional dev-fallback strings, not production secrets:
backend_v2/api/__init__.py:57:_secret_key = 'dev_key_for_development_only'— only reached whenFLASK_ENV in ('development','testing'). Production path raisesRuntimeErrorimmediately ifSECRET_KEYis unset (this was the C4 fix from the 2026-04-24 security review; the guard is correct).console/app/__init__.py:30: same pattern;_secret = "dev_key_for_development_only".-
Remaining
B105hits in blueprints/services: all are placeholder strings used as fallback sentinel values in non-production code paths (e.g.,ROTATION_CALLBACK_SECRETguard,FREESCOUT_HMAC_KEYguard). Each raises or short-circuits before any authenticated operation if the real secret is absent. -
Classification: ALLOWLIST
- Severity: LOW
- Reasoning: All dev-only fallbacks confirmed; production paths guard via env-var check + RuntimeError. No hardcoded credential reachable from a production request path.
- Action: Add B105 suppression for
*/app/__init__.pydev paths in.banditconfig.
B106 — hardcoded password function arguments (3 occurrences)
console/app/blueprints/secrets.py:1164,2067:rolling_token_env_name="POSTMARK_SERVER_TOKEN_PENDING"— this is a string argument passing an environment variable name, not a credential value. Bandit's keyword matching on_token_in the argument name triggers the rule.-
console/app/services/vault.py:813:secret_path="/"— the string"/"is a path argument, not a password. Bandit matches onsecret_prefix. -
Classification: ALLOWLIST
- Severity: LOW
- Reasoning: No credential values are hardcoded. These are variable names and path strings that happen to match bandit's password-parameter heuristic.
- Action: Add
# nosec B106inline comments at these three callsites (feature-developer task).
B101 — assert used in backend_v2/observability_checks.py:64
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning:
assert self._client is not Noneguards a test-mode in-process client object. Thein_processflag is only true during unit test runs (the constructor setsself._client = app.test_client()in that branch). This is observability check tooling, not a security gate. Assert removal would not create an exploitable path; it would produce anAttributeErrorinstead. - Action: Add
# nosec B101inline comment (feature-developer task, low priority).
B404/B603/B607 — subprocess in backend_v2/conftest.py
- Classification: ALLOWLIST
- Severity: LOW
- Reasoning:
backend_v2/conftest.pyis test infrastructure. Thesp_runinvocation at line 108 is inside a pytest fixture that spins up a local dev server. Test infra subprocess usage perfeedback_bandit_in_tests_policy— non-exploitable. - Action: Add conftest.py path to
.banditskip-in-tests config.
FIX-CODE findings — dispatch to feature-developer (5 items)
FIX-CODE #1: npm audit HIGH — tmp path traversal (GHSA-ph9p-34f9-6g65)
- Scanner: npm-audit
- Finding:
tmp@0.2.5(range:<0.2.6) — path traversal via unsanitizedprefix/postfix, CWE-22 - Advisory:
https://github.com/advisories/GHSA-ph9p-34f9-6g65 - Dependency chain:
selenium-webdriver@4.29.0(direct devDependency) →tmp@0.2.5 - Classification: REAL-VULN
- Severity: HIGH (GHSA rating; CVSS score not populated in advisory)
- Reasoning:
tmpis pulled in byselenium-webdriverwhich is a directdevDependencyinfrontend/trademaster_ui/package.json. It is NOT in the production bundle —selenium-webdriveris markeddev: truein the lockfile. However,tmpwrites files to the filesystem during test/CI runs, and a craftedprefix/postfixcould escape the temp directory during CI. Given the CI environment has access to the repo checkout and deploy credentials, this is a real risk surface in CI even if not in the production browser bundle. - Action: Feature-developer should update
selenium-webdriverto a version that pinstmp>=0.2.6, OR add anoverridesentry infrontend/trademaster_ui/package.json:"overrides": { "tmp": ">=0.2.6" }. Verify withnpm auditafter change. - BLOCKS-MERGE on #3214 until resolved.
FIX-CODE #2: npm audit MEDIUM — react-router-dom open redirect (GHSA-2j2x-hqr9-3h42)
- Scanner: npm-audit
- Finding:
react-router-dom@^6.30.3(direct dependency) →react-routeropen redirect via//-prefixed same-origin paths reinterpreted as protocol-relative URLs - Advisory:
https://github.com/advisories/GHSA-2j2x-hqr9-3h42 - Classification: FIX-CODE
- Severity: MEDIUM (GHSA moderate; open redirect, not RCE)
- Reasoning: Direct dependency. Open redirect via
//evil.com/paths is a real user-facing risk — phishing vectors that pass same-origin checks. Antlers handles routing; any redirect target constructed from user input (e.g.,?redirect=params) is the attack vector. - Action: Feature-developer should upgrade
react-router-domto the patched version. Checknpm auditadvisory for patched range and updatepackage.json. - Does not block merge of scan PRs but should be tracked as MEDIUM.
FIX-CODE #3: npm audit MEDIUM — postcss (GHSA-qx2v-qp2m-jg93)
- Scanner: npm-audit (present in #3194, #3197, #3214)
- Finding:
postcsstransitive dep with known advisory - Advisory:
https://github.com/advisories/GHSA-qx2v-qp2m-jg93 - Classification: FIX-CODE
- Severity: MEDIUM
- Reasoning:
postcssis a direct devDependency (^8.4.31) and also pulled transitively. The advisory at GHSA-qx2v-qp2m-jg93 covers a ReDoS vulnerability in PostCSS's parser. Build-time only; not in the production bundle. - Action: Feature-developer should upgrade
postcssto>=8.5.4(check advisory for exact patched version).npm audit fixlikely resolves this.
FIX-CODE #4: bandit B608 MEDIUM — dynamic SQL in backend_v2/api/routes/strategies.py (hardening opportunity)
- Scanner: bandit B608
- Finding:
f"UPDATE strategies SET {set_clause} WHERE id = :_sid AND user_id = :_uid"at line ~573 - Classification: FIX-CODE (hardening)
- Severity: MEDIUM (scanner-rated; exploitability LOW given allow-list, but the pattern warrants hardening)
- Reasoning: Although
set_clausecolumn names are drawn from_RULE_COLUMNS(hardcoded tuple) and three explicitly named fields, the dynamic construction pattern is error-prone for future maintainers who might add non-allowlisted keys. A future developer could add abody.get()call that bypasses the allowlist. The correct fix is to assert/validate that every key inupdatesis in a combined allowlist before buildingset_clause, making the defense explicit rather than implicit. - Action: Feature-developer should add an explicit allowlist assertion before
set_clauseconstruction:assert all(k in _ALLOWED_UPDATE_COLS for k in updates)where_ALLOWED_UPDATE_COLS = frozenset(_RULE_COLUMNS) | {"name", "strategy_type", "description", "is_active", "updated_at"}. Same pattern should be applied to auth.py'scontact_updatespath. - Does not block merge of scan PRs. Treat as MEDIUM hardening ticket.
- STATUS: RESOLVED — allowlist-asserted in
strategies.py(_ALLOWED_UPDATE_COLS) andauth.py(_ALLOWED_CONTACT_COLS); ValueError raised on unexpected column; tests added. See PR #3215.
FIX-CODE #5: bandit config — add .bandit project-wide skip-in-tests + known-FP suppressions
- Scanner: bandit (all 5 PRs)
- Finding: 57 ALLOWLIST findings repeat every night because there is no
.banditconfig suppressing confirmed FPs - Classification: FIX-CODE (tooling)
- Severity: LOW (signal-to-noise problem, not an exploit)
- Reasoning: Per
feedback_bandit_in_tests_policy, a bandit project config (setup.cfgor.banditfile) should suppressB101, B106, B110, B112for*/tests/*paths and suppress confirmed FPs per the analysis above. Without this, every nightly run files 50+ allowlist-grade findings and drowns real signals (#2427 tracks this). - Action: Feature-developer should create/update
setup.cfgwith a[bandit]section (or.banditconfig) that adds: skips = B101for*/tests/*,*/conftest.py,*/observability_checks.pyskips = B105for*/app/__init__.py(dev-fallback pattern)skips = B608for*/alembic/versions/*,*/scripts/*skips = B110,B112for*/migrations/*paths- Per-file
# nosec B106comments at the three vault/secrets callsites identified above - Does not block merge. Queue as part of #2427.
Consolidated patterns (apply across all 5 PRs)
-
B110/B112 cluster (38 files, ~52 groups) — entire bandit B110/B112 block across
console/andbackend_v2/is non-exploitabletry/exceptdefensive coding. Apply.banditskip config; close all related nightly-scan issues that matchB110orB112. Do NOT suppress in production auth/crypto paths — verify on any future new occurrence. -
B608 in constants-only SQL templates — four locations confirmed safe (migration 0026, flag_reconciler_backfill.py, auth.py CONTACT_KEYS, strategies.py _RULE_COLUMNS). Pattern: bandit fires on any f-string that contains SQL keywords; it does not verify whether the interpolated value is a constant or user-supplied. Add per-file
noqa: S608or.banditpath skips for each confirmed location. -
B105/B106 dev-fallback keys — all confirmed as intentional dev-env-only sentinel strings that raise in production. No action beyond
.banditconfig suppression. -
npm audit LOW cluster (jest/jsdom/react-scripts, present in #3170 and #3184) —
@jest/core,jest,jest-cli,jest-config,jest-environment-jsdom,jest-runner,jsdom,react-scripts,http-proxy-agent,@tootallnate/onceadvisory cluster. All are devDependencies in the CRA test harness. Not present in later scans (#3194+) suggesting an advisory retraction or version bump. No action needed; these do not affect the production bundle. -
Scan count drift (82 → 72 → 73): The 10-finding drop between #3184 and #3194 corresponds to the npm audit block changing (jest cluster advisories dropped,
next/postcsscluster appeared). The +1 between #3197 and #3214 is a newB110atbackend_v2/api/services/mbt_fill_engine.py— confirmed astry/except: passat line 475 inside a P&L decimal-conversion helper. ALLOWLIST.
Pre-existing security issue inventory
20 open type:security issues as of triage time. The SRE-agent should cross-reference after merging #3214:
| Issue | Title | Status |
|---|---|---|
| #2285 | SC-WAF-05b: WAF prod rollout | blocked + defer:post-launch |
| #2283 | SC-WAF-05: WAF staging block mode | blocked + defer:post-launch |
| #2282 | SC-WAF-04: WAF staging challenge mode | blocked + needs:operator-decision |
| #2129 | SC-12 prep: Infisical vault paths for Ed25519 keys | blocked + operator-action |
| #1869 | security(burr): audit + pentest Burr v1 | blocked + defer:post-launch |
| #1742 | SC-WAF-08/09: AWS WAF + Velvet Logpush | blocked + defer:post-launch |
| #1736 | SC-WAF-00: CF account WAF settings | ready-for-dev + defer:post-launch |
| #1735 | HIGH: No CF WAF rules configured | blocked + needs:operator-decision |
| #1694 | feat(console): break-glass nightly snapshot | blocked + defer:post-launch |
| #1692 | feat(console): break-glass session flow | blocked + defer:post-launch |
| #1357 | HIGH: bandit hardcoded_sql_expressions admin_customers.py | recommend-close |
| #954 | feat(velvet/ui): yaml-driven revocation auth gate | defer:post-launch |
| #596 | ops(vault): Phase 1 — audit per-secret env coverage | defer:post-launch |
| #595 | ops(vault): Phase 2 — vault_env_gap_fill.py | blocked + defer:post-launch |
| #453 | feat: sync Founders waitlist → CF Access policy | blocked + defer:post-launch |
| #451 | feat(console): FLAG_ENFORCE_CF_ORIGIN toggle | blocked + needs:operator-decision |
| #253 | Epic: Automated credential rotation pipelines | blocked + defer:post-launch |
| #251 | Security H3 — rotate HEROKU_API_KEY | defer:post-launch + operator-action |
| #250 | Epic: Passkey E2E encryption | blocked + defer:post-launch |
Note: #1357 (hardcoded_sql_expressions at admin_customers.py) already carries recommend-close label. This triage confirms the underlying B608 pattern is a false positive (allow-listed constants). SRE should close #1357 with a comment referencing this plan.
Handoff to SRE-agent
The SRE-agent should:
-
Read this triage plan at
docs/security/remediation/2026-06-04-nightly-scans-triage.md -
For each older PR, apply the close-out path: - PR #3170: close with comment "Superseded by #3214. Triage: docs/security/remediation/2026-06-04-nightly-scans-triage.md" - PR #3184: same comment - PR #3194: same comment - PR #3197: same comment
-
For PR #3214 — DO NOT MERGE until FIX-CODE #1 (
tmppath traversal) is resolved by feature-developer. After that fix lands: - Merge #3214 into main - Comment on merge: "Triage complete per docs/security/remediation/2026-06-04-nightly-scans-triage.md" -
Dispatch to feature-developer (in priority order): - P1 (blocks #3214 merge): FIX-CODE #1 — update
selenium-webdriveror addoverrides.tmpinfrontend/trademaster_ui/package.jsonto resolvetmp@<0.2.6HIGH finding - P2 (MEDIUM, do not block merge): FIX-CODE #2 — upgradereact-router-dompast open-redirect advisory - P3 (MEDIUM, build-time only): FIX-CODE #3 — upgradepostcssto patched version - P4 (hardening): FIX-CODE #4 — explicit allowlist assertion beforeset_clauseconstruction in strategies.py and auth.py - P5 (tooling debt, tracks #2427): FIX-CODE #5 —.banditproject config to suppress confirmed FPs -
Close existing security issue #1357 (
recommend-closealready labeled) — B608 false positive confirmed by this triage. -
Run
gh issue list --label type:securityafter merging #3214 and verify no new scan-generated issues remain open beyond the 19 pre-existing deferred issues catalogued above.
Estimated effort: 1–2 hours SRE coordination + 2–3 hours feature-developer for P1–P3.
Blocking handoffs: feature-developer must resolve FIX-CODE #1 before SRE merges #3214.
No new GH issues filed by this triage — findings either match pre-existing issues or are policy-allowlisted.