chore(ci): add semgrep guard against prototype pollution regressions in provider hot paths (#78)

* chore(ci): add semgrep rule no-bracket-assign-on-literal-object-map

* chore(ci): add workflow running semgrep bracket-assign guard on push/PR

* fix(parser): use Object.create(null) for categoryBreakdown map

* chore(ci): expand semgrep rule to cover ||, ??=, and if-guard variants

* chore(ci): limit push trigger to main and add semgrep --strict

* chore(ci): use jq to enforce finding count (--error unreliable in semgrep 1.x)
This commit is contained in:
Ninym 2026-04-19 00:10:24 +02:00 committed by GitHub
parent a031c8d32d
commit 5932a273a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 50 additions and 1 deletions

27
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Semgrep
run: pip install semgrep
- name: Run Semgrep bracket-assign guard
run: |
set -e
semgrep --config .semgrep/rules/no-bracket-assign-hot-paths.yml \
--strict --json \
src/providers/ src/parser.ts > semgrep-out.json
FINDINGS=$(jq '.results | length' semgrep-out.json)
if [ "$FINDINGS" -gt 0 ]; then
jq -r '.results[] | "::error file=\(.path),line=\(.start.line)::\(.extra.message)"' semgrep-out.json
exit 1
fi

View file

@ -0,0 +1,22 @@
rules:
- id: no-bracket-assign-on-literal-object-map
languages: [typescript]
severity: ERROR
message: >
Bracket-assign on a map created with `{}` allows prototype pollution when
the key comes from external data. Initialize the map with
`Object.create(null)` instead.
patterns:
- pattern-either:
- pattern: $MAP[$KEY] = $MAP[$KEY] ?? $INIT
- pattern: $MAP[$KEY] = $MAP[$KEY] || $INIT
- pattern: $MAP[$KEY] ??= $INIT
- pattern: |
if (!$MAP[$KEY]) $MAP[$KEY] = $INIT
- pattern-not-inside: |
const $MAP = Object.create(null)
...
paths:
include:
- '/src/providers/*.ts'
- '/src/parser.ts'

View file

@ -173,7 +173,7 @@ function buildSessionSummary(
const toolBreakdown: SessionSummary['toolBreakdown'] = Object.create(null)
const mcpBreakdown: SessionSummary['mcpBreakdown'] = Object.create(null)
const bashBreakdown: SessionSummary['bashBreakdown'] = Object.create(null)
const categoryBreakdown: SessionSummary['categoryBreakdown'] = {} as SessionSummary['categoryBreakdown']
const categoryBreakdown: SessionSummary['categoryBreakdown'] = Object.create(null)
let totalCost = 0
let totalInput = 0