mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:32:02 +00:00
fix(bundle-8): Frontend Hardening — 2 audit findings closed + 3 partial
Closes Audit-2026-04-25 L-015 (Low) and L-019 (Low) — both
verified-already-clean at HEAD; new CI regression guards prevent
regression. Partial closures for M-009, M-010, M-026 — Bundle 8 ships
the helpers + contract tests + a soft CI budget guard, defers the
long-tail per-page migrations to a new tracker ID M-029.
What changed
- web/src/utils/safeHtml.ts (NEW) — sanitizeHtml() chokepoint for
any future code that genuinely needs dangerouslySetInnerHTML.
Bundle-8 placeholder body throws — DOMPurify dependency is the
activation procedure documented in the file header.
- web/src/components/ExternalLink.tsx (NEW) — single chokepoint for
target="_blank" anchors. Hardcodes rel="noopener noreferrer".
- web/src/hooks/useListParams.ts (NEW) — URL-state hook for filter /
sort / pagination state on list pages. Canonicalises the existing
DashboardPage useSearchParams pattern. Per-page migrations of the
~14 remaining list pages tracked as M-029.
- web/src/hooks/useTrackedMutation.ts (NEW) — useMutation wrapper
enforcing the M-009 invalidation contract via discriminated-union
type: caller MUST declare invalidates: QueryKey[] OR
invalidates: 'noop' + noopReason: string.
- 4 new Vitest test files — full unit coverage for ExternalLink
(target/rel preservation), safeHtml (placeholder throws + activation
hint), useListParams (URL contract / defaults / filter-resets-page),
useTrackedMutation (invalidate-then-onSuccess / noop variant).
- .github/workflows/ci.yml — three new regression guards:
Bundle-8 / L-015: greps for any target="_blank" outside ExternalLink
that lacks rel="noopener noreferrer"; clean at HEAD.
Bundle-8 / L-019: greps for any dangerouslySetInnerHTML outside
safeHtml.ts; clean at HEAD (0 sites).
Bundle-8 / M-009: SOFT budget guard — useMutation sites must not
exceed invalidation sites + 5. At HEAD: 61 mutations vs 82
invalidations + 5 = 87 budget. Stricter per-site enforcement
tracked as M-029.
Verification at HEAD
- web/src/ target=_blank sites: 3 (all in OnboardingWizard.tsx)
— all three already carry rel="noopener noreferrer". L-015 closed.
- web/src/ dangerouslySetInnerHTML sites: 0. L-019 closed.
- useMutation sites: 61 / invalidateQueries: 82 (M-009 budget healthy)
Per-finding mapping
- L-015 closed (CWE-1022) — verified-already-clean + ExternalLink
component + CI grep guard.
- L-019 closed (CWE-79) — verified-already-clean + safeHtml chokepoint
+ CI grep guard.
- M-009 partial — useTrackedMutation wrapper authored; soft CI budget
guard. Migrating the 56 existing useMutation sites to the wrapper
tracked as M-029.
- M-010 partial — useListParams hook authored + tested. Per-page
migration of the ~14 list pages tracked as M-029.
- M-026 partial — bundle-prompt called for XSS-hardening tests on the
T-1 deferred allowlist of 14 pages. Bundle 8 ships the testing
pattern via the new helpers but does NOT execute the per-page
migrations — tracked as M-029.
NOT addressed in this bundle (deferred to M-029)
- Migrating existing 56 useMutation sites to useTrackedMutation
- Migrating ~14 list pages from local useState to useListParams
- Adding XSS-hardening tests to the 14 T-1-deferred pages
Verification
- npx tsc --noEmit → clean
- npx vitest run on the 4 new Bundle-8 test files → 15/15 pass
- L-015 grep guard simulation → clean
- L-019 grep guard simulation → clean
- M-009 budget simulation → 61 ≤ 87 (clean)
- go vet ./... → clean (no backend changes)
- python3 yaml.safe_load(api/openapi.yaml) → clean
- python3 yaml.safe_load(.github/workflows/ci.yml) → clean
Backwards compatibility
- All 4 new helper files are additive; no existing call sites were
modified. Existing list pages keep their useState pagination until
M-029 ships per-page migrations.
Bundle 8 of the 2026-04-25 comprehensive audit. Per-page migration
backlog tracked as new audit finding M-029.
This commit is contained in:
@@ -825,6 +825,77 @@ jobs:
|
||||
ALLOWLIST_SIZE=$(echo "$ALLOW" | tr '|' '\n' | wc -l)
|
||||
echo "T-1 page-coverage guardrail: clean (allowlist size: $ALLOWLIST_SIZE pages deferred)."
|
||||
|
||||
- name: Bundle-8 / L-015 target=_blank rel=noopener regression guard
|
||||
# Audit L-015 / CWE-1022 (reverse-tabnabbing): every <a target="_blank">
|
||||
# MUST carry rel="noopener noreferrer" so a malicious page at the
|
||||
# target URL cannot navigate the opener window via window.opener.
|
||||
# At Bundle-8 close (commit b566355→) all 3 sites in the codebase
|
||||
# already comply — this guard prevents regression. The
|
||||
# ExternalLink component (web/src/components/ExternalLink.tsx)
|
||||
# is the recommended way to add new external links.
|
||||
run: |
|
||||
set -e
|
||||
OFFENDERS=$(grep -rnE 'target=["'"'"']?_blank["'"'"']?' web/src/ 2>/dev/null \
|
||||
| grep -v 'noopener noreferrer' \
|
||||
| grep -v 'web/src/components/ExternalLink.tsx' \
|
||||
|| true)
|
||||
if [ -n "$OFFENDERS" ]; then
|
||||
echo "L-015 regression: target=\"_blank\" without rel=\"noopener noreferrer\":"
|
||||
echo "$OFFENDERS"
|
||||
echo ""
|
||||
echo "Either add rel=\"noopener noreferrer\" inline,"
|
||||
echo "or migrate to <ExternalLink> from web/src/components/ExternalLink.tsx."
|
||||
exit 1
|
||||
fi
|
||||
echo "L-015 target=_blank guardrail: clean."
|
||||
|
||||
- name: Bundle-8 / L-019 dangerouslySetInnerHTML regression guard
|
||||
# Audit L-019 / CWE-79 (XSS): no production code may use
|
||||
# dangerouslySetInnerHTML directly. At Bundle-8 close the codebase
|
||||
# has 0 sites; future genuine needs MUST route through
|
||||
# web/src/utils/safeHtml.ts::sanitizeHtml.
|
||||
run: |
|
||||
set -e
|
||||
OFFENDERS=$(grep -rnE 'dangerouslySetInnerHTML' web/src/ 2>/dev/null \
|
||||
| grep -v 'web/src/utils/safeHtml.ts' \
|
||||
|| true)
|
||||
if [ -n "$OFFENDERS" ]; then
|
||||
echo "L-019 regression: dangerouslySetInnerHTML used outside safeHtml.ts:"
|
||||
echo "$OFFENDERS"
|
||||
echo ""
|
||||
echo "Route through web/src/utils/safeHtml.ts::sanitizeHtml — see file"
|
||||
echo "header for the activation procedure (DOMPurify dependency)."
|
||||
exit 1
|
||||
fi
|
||||
echo "L-019 dangerouslySetInnerHTML guardrail: clean."
|
||||
|
||||
- name: Bundle-8 / M-009 mutation invalidation contract guard
|
||||
# Audit M-009: every useMutation must either invalidate the
|
||||
# queries it changes OR document why no invalidation is needed.
|
||||
# SOFT guard — counts useMutation sites and asserts the budget
|
||||
# doesn't grow without a corresponding invalidateQueries / setQueryData /
|
||||
# useTrackedMutation reference. Stricter per-site enforcement is
|
||||
# tracked as M-029 (covers the long-tail useListParams + useTrackedMutation
|
||||
# migration of the existing 56 useMutation sites).
|
||||
run: |
|
||||
set -e
|
||||
MUTATIONS=$(grep -rcE 'useMutation\(|useTrackedMutation\(' web/src/ 2>/dev/null \
|
||||
| awk -F: '{s+=$2} END{print s}')
|
||||
INVALIDATIONS=$(grep -rcE 'invalidateQueries|setQueryData|removeQueries|invalidates:' web/src/ 2>/dev/null \
|
||||
| awk -F: '{s+=$2} END{print s}')
|
||||
echo "M-009 budget — useMutation sites: $MUTATIONS / invalidation sites: $INVALIDATIONS"
|
||||
# At Bundle-8 close: 56 useMutation + 70 invalidation. We allow
|
||||
# +5 mutations growth before requiring invalidation parity. If
|
||||
# the gap widens, audit the new mutation sites for missing
|
||||
# invalidation pairs.
|
||||
BUDGET=$((INVALIDATIONS + 5))
|
||||
if [ "$MUTATIONS" -gt "$BUDGET" ]; then
|
||||
echo "M-009 regression: $MUTATIONS useMutation sites exceeds invalidation budget ($BUDGET)."
|
||||
echo "New mutations should pair with invalidateQueries/setQueryData OR migrate to"
|
||||
echo "useTrackedMutation (web/src/hooks/useTrackedMutation.ts) with explicit invalidates."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Forbidden env-var docs drift regression guard (G-3)
|
||||
# G-3 master closed cat-g-163dae19bc59 (docs-only env vars
|
||||
# phantom in features.md), cat-g-b8f8f8796159 (6 config-only
|
||||
|
||||
Reference in New Issue
Block a user