From 5932a273a18ecf19979e5f0b4542cb537729842d Mon Sep 17 00:00:00 2001 From: Ninym <80782621+lfl1337@users.noreply.github.com> Date: Sun, 19 Apr 2026 00:10:24 +0200 Subject: [PATCH] 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) --- .github/workflows/ci.yml | 27 +++++++++++++++++++ .../rules/no-bracket-assign-hot-paths.yml | 22 +++++++++++++++ src/parser.ts | 2 +- 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .semgrep/rules/no-bracket-assign-hot-paths.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5233243 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/.semgrep/rules/no-bracket-assign-hot-paths.yml b/.semgrep/rules/no-bracket-assign-hot-paths.yml new file mode 100644 index 0000000..e2e633d --- /dev/null +++ b/.semgrep/rules/no-bracket-assign-hot-paths.yml @@ -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' diff --git a/src/parser.ts b/src/parser.ts index 4adcf3b..ba0d31c 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -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