diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ce929df..2acf87a1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,8 +28,34 @@ jobs: # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Recompile with: # uv pip compile --generate-hashes --universal --python-version 3.11 requirements-test.in -o requirements-test.txt run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-test.txt + - name: Install Playwright browsers + run: python -m playwright install chromium - name: Run tests + if: matrix.python-version != '3.13' run: python -m pytest -q + - name: Run tests with dashboard browser evidence + if: matrix.python-version == '3.13' + env: + APPGUARDRAIL_UI_EVIDENCE_DIR: artifacts/ui + run: python -m pytest -q + - name: Bind dashboard browser evidence to this run + if: matrix.python-version == '3.13' + env: + REQUESTED_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + mkdir -p artifacts/ui + { + printf 'requested_pr_head_sha=%s\n' "$REQUESTED_PR_HEAD_SHA" + printf 'checkout_sha=%s\n' "$(git rev-parse HEAD)" + } > artifacts/ui/revision.txt + - name: Upload dashboard browser evidence + if: matrix.python-version == '3.13' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dashboard-browser-evidence-${{ github.event.pull_request.head.sha || github.sha }} + path: artifacts/ui/ + if-no-files-found: error + retention-days: 14 - name: Verify 100% statement coverage for live drift modules if: matrix.python-version == '3.13' run: | diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1600d432..c816f454 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -127,3 +127,12 @@ **Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads. **Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails. **Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors. +## 2026-09-08 - Dashboard untrusted JSON rendering boundary +**Vulnerability:** Scan counts and a scan identifier from the control-plane JSON response were interpolated into `innerHTML` without an explicit numeric or HTML-attribute boundary. A malformed or corrupted response could therefore turn those values into markup at the dashboard sink. +**Learning:** API provenance does not replace contextual output encoding. Numeric display values must be converted to numbers and string attributes must use the existing HTML escape helper. Dynamic severity lookup is property-selection confusion, not prototype mutation: this path reads from the fixed `SEV` palette and does not write an object or its prototype. +**Prevention:** Coerce count fields with `Number(...)`, escape the scan identifier before inserting it into `data-id`, and admit a severity color only when the normalized key is an own property of `SEV`; otherwise use the information fallback. Preserve these boundaries in the consolidated dashboard security contract and add browser hostile-payload evidence before declaring the UI slice complete. + +## 2026-09-08 - Treat dashboard counts as non-negative safe integers +**Vulnerability:** Untrusted control-plane count fields reached `innerHTML` and trend geometry without a single typed boundary; a partial parser could also accept strings such as `12px` or truncate fractions. +**Learning:** `parseInt` is not validation. Convert the whole value with `Number`, then admit only `Number.isSafeInteger(value) && value >= 0`. Malformed, fractional, negative and infinite values render as zero. +**Prevention:** Reuse the one bounded `count()` projection for stats, history cells, pills and trend height/color. Keep text/identity fields on `esc()` and severity palette selection on `Object.hasOwn`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 110a549d..4b097df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 대시보드 검색창 커서 유지 — 검색어 중간에서 텍스트를 수정할 때마다 커서가 검색어의 맨 끝으로 점프하는 불편함을 수정했습니다. 이제 입력창의 커서 위치(`selectionStart`/`selectionEnd`)가 동적 렌더링 이후에도 원래 위치에 정확히 유지되어 자연스러운 타이핑 경험을 제공합니다. ### 보안 +- Control-plane 대시보드의 JSON 렌더링 경계를 보강했습니다. 스캔 count는 숫자로 강제하고 `data-id`는 HTML 속성 이스케이프를 적용하며 severity 색상은 고정 palette의 own property만 허용합니다. 기존 dashboard security contract가 이 positive/negative 경계를 고정합니다. Count fields now pass through one non-negative safe-integer projection; malformed, fractional, negative and infinite values render as zero rather than markup or invalid trend geometry. - 리포트 출력 하드닝 — 생성된 markdown 리포트가 HTML로 렌더될 때 악성 finding 내용(예: 외부 엔진이 스캔한 코드의 `