Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 65 additions & 6 deletions .github/instructions/xss-prevention.instructions.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
applyTo: '**/*.js, **/*.html, **/*.py'
applyTo: '**/*.js, **/*.html, **/*.py, **/*.ts, **/*.tsx, **/*.jsx, **/*.mjs'
---

# Security: XSS Prevention and Browser Rendering
Expand Down Expand Up @@ -110,24 +110,83 @@
- It contains no `${...}` interpolation or dynamic concatenation
- Untrusted values are populated afterward with `textContent`, `setAttribute(...)`, or `dataset`

## React and TypeScript (V2 UI)

The V2 UI in `application/v2_ui/src` is React 18 with TypeScript. React removes most string-to-HTML risk, but not all of it, so the same trust-boundary thinking applies.

### What React 18 escapes

- Text rendered as JSX children, such as `<p>{message.text}</p>`, is inserted as text, never parsed as HTML.
- String values in attributes are escaped, so a quote in a value cannot break out of the attribute.

### What React 18 does not protect

- **URL-bearing props.** React 18 only logs a development warning for a `javascript:` URL in `href`, `src`, `action` or `formAction`; React 19 blocks it, React 18 does not. Treat these props, plus `srcDoc`, `xlinkHref` and router `to` on `Link`, `NavLink` and `Navigate`, as sinks.
- **`dangerouslySetInnerHTML`.** The value is parsed as HTML with no escaping.

Check warning on line 125 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
- **Refs and DOM APIs.** `ref.current.innerHTML`, `outerHTML`, `insertAdjacentHTML`, `document.write`, `createContextualFragment`, and `DOMParser` output appended to the page all bypass React.

Check warning on line 126 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
- **DOM URL properties.** `element.href`, `element.src`, `form.action`, `frame.srcdoc` and `setAttribute('href', ...)` are not React props, so React never inspects them.
- **Navigation.** `window.open(url)`, `location.href = url`, `location = url`, and `location.assign(url)` or `location.replace(url)` can run a `javascript:` URL with the page's origin.
- **String-to-code APIs.** `eval`, `new Function`, and `setTimeout`/`setInterval` with a string argument.

Check warning on line 129 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Moderate - Changed line contains obfuscation, dynamic loading, or hidden payload marker. Recommendation%3A Confirm the changed code is not hiding behavior, decoding payloads, or bypassing normal review.

### Required patterns

- Render untrusted text as JSX children. Never route text through `dangerouslySetInnerHTML` to get formatting.

Check warning on line 133 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
- Use `dangerouslySetInnerHTML` only with the output of `DOMPurify.sanitize(...)`, either inline or through a clearly named `const`. Rich renderers must also run their own safe mode first: KaTeX with `trust: false`, Mermaid with `securityLevel: 'strict'` and `htmlLabels: false`.

Check warning on line 134 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
- Give every dynamic URL prop or navigation value one of these safe sources:
- a literal, or a same-origin path built from a literal prefix and `encodeURIComponent(...)` segments;
- a reviewed URL builder (the lists are `TS_SAME_ORIGIN_URL_BUILDERS` and `TS_HREF_URL_BUILDERS` in `scripts/check_xss_sinks.py`; add a builder there only after reviewing that it cannot return a script URL);
- a `sanitize*Url`, `normalize*Url` or `safe*Url` helper;
- a guard such as `isSafeOriginHref(url)` or an `http(s)` scheme test, in the same expression or in an enclosing `if` or early return.
- `iframe`, `script`, `embed`, `object` and worker sources are stricter. A pinned `https://host/` prefix or a same-origin path with encoded segments is fine; an arbitrary host or `//` is not.

Check warning on line 140 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains external connection or remote asset marker. Recommendation%3A Review whether changed code can send prompts, files, credentials, cookies, settings, logs, or user data to a new sink.
- Keep `react-markdown` raw HTML off. Do not add `rehype-raw`, `allowDangerousHtml: true`, or a `urlTransform` that returns the URL unchanged; keep the default URL transform.
- `<img src>` and media sources are not script sinks, so the checker does not flag them. Still pass them through the same helpers when the value comes from a user.

Check warning on line 142 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

### React examples

```tsx
<p>{message.text}</p>
<a href={isSafeOriginHref(link) ? link : undefined}>Open</a>
<Link to={`/workspace/${encodeURIComponent(workspaceId)}`}>Open workspace</Link>
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(renderedSvg) }} />

Check warning on line 150 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
```

Unsafe, because nothing proves the value's scheme or origin:

Check warning on line 153 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains dynamic execution, persistence, or system access marker. Recommendation%3A Do not execute changed lifecycle scripts or installers while this finding is unresolved.

```tsx
<a href={message.link}>Open</a>
<div dangerouslySetInnerHTML={{ __html: modelOutput }} />

Check warning on line 157 in .github/instructions/xss-prevention.instructions.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
window.location.href = searchParams.get('next') ?? '/';
```

### Suppressions in TypeScript

In `.ts`, `.tsx`, `.jsx` and `.mjs` files the suppression token counts only when a reason follows it on the same line. Put it within four lines above the finding:

```tsx
{/* xss-check: ignore reviewed: value is restricted to https URLs before it is stored */}
<a href={agent.homepage}>Homepage</a>
```

A bare token does not suppress anything; the finding stays and says the reason is missing.

## PR Review Checklist

For any JavaScript, HTML, or Python change that affects browser rendering:
For any JavaScript, TypeScript, HTML, or Python change that affects browser rendering:

1. Identify the trust boundary for every value that reaches the browser.
2. Prefer DOM node creation and `textContent` for untrusted text.
3. Normalize dynamic URLs before assigning them to clickable or loadable attributes.
2. Prefer DOM node creation and `textContent` for untrusted text; in React, render it as JSX children.
3. Normalize dynamic URLs before assigning them to clickable or loadable attributes, React props, or navigation calls.
4. If HTML rendering is required, document the sanitizer boundary explicitly.
5. Add or update a regression test when untrusted data reaches a browser-rendering path.

## Workflow Guardrail

This repository includes a Development PR check in `.github/workflows/xss-sink-check.yml` backed by `scripts/check_xss_sinks.py`.
This repository includes a PR check in `.github/workflows/xss-sink-check.yml` backed by `scripts/check_xss_sinks.py`. It runs on PRs into `Development` and, until the V2 branch merges into `Development`, on PRs into `paullizer-react-v2-ui`. It checks the added lines of changed `.js`, `.html`, `.py`, `.ts`, `.tsx`, `.jsx` and `.mjs` files under `application/`, and skips vendored browser assets such as `application/v2_ui/public/vendor/`. `.github/workflows/xss-full-scan.yml` runs the same checker over whole files on demand.

If a reviewed exception is unavoidable, add the suppression token below near the specific line and include a justification comment:

```text
xss-check: ignore
```

Use that escape hatch rarely. It is for reviewed legacy exceptions, not for normal rendering code.
Use that escape hatch rarely. It is for reviewed legacy exceptions, not for normal rendering code. In TypeScript-family files the justification must follow the token on the same line.
5 changes: 3 additions & 2 deletions .github/prompts/prepare-for-pull-request.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ When Python route files changed:
`python functional_tests/route_tests/test_route_unauthenticated_policy_contract.py`
`python functional_tests/route_tests/test_route_policy_test_coverage.py`

When JavaScript, HTML, or Python browser-rendering surfaces changed:
When JavaScript, TypeScript, HTML, or Python browser-rendering surfaces changed:

- Run `python scripts/check_xss_sinks.py` against changed `application/**/*.js`, `application/**/*.html`, and `application/**/*.py` files.
- Run `python scripts/check_xss_sinks.py` against changed `application/**/*.js`, `application/**/*.html`, `application/**/*.py`, `application/**/*.ts`, `application/**/*.tsx`, `application/**/*.jsx`, and `application/**/*.mjs` files.
- If validating only committed changes, prefer changed-line mode:
`python scripts/check_xss_sinks.py --base-sha <remote>/Development --head-sha HEAD <files>`.
For a PR into the temporary V2 branch `paullizer-react-v2-ui`, use `<remote>/paullizer-react-v2-ui` as the base instead.
- If the working tree has uncommitted changes, use `--full-file` for those files because they are not represented by `HEAD`.

When Python authorization-sensitive surfaces changed:
Expand Down
49 changes: 41 additions & 8 deletions .github/prompts/xss-full-audit-and-remediation.prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ agent: "agent"

# XSS Full Audit And Remediation

Run a full local SimpleChat XSS audit and remediation workflow. This is broader than the GitHub Actions PR check in [.github/workflows/xss-sink-check.yml](../workflows/xss-sink-check.yml): the action checks changed application lines, while this prompt should inspect the whole browser-rendering surface and produce a plan to fix real risks.
Run a full local SimpleChat XSS audit and remediation workflow. This is broader than the GitHub Actions PR check in [.github/workflows/xss-sink-check.yml](../workflows/xss-sink-check.yml): the action checks changed application lines, while this prompt should inspect the whole browser-rendering surface and produce a plan to fix real risks. The on-demand [.github/workflows/xss-full-scan.yml](../workflows/xss-full-scan.yml) workflow runs the same checker over whole files and is a useful first pass, but it does not replace the manual review below.

Use the repository guardrails in [.github/instructions/xss-prevention.instructions.md](../instructions/xss-prevention.instructions.md), [.github/instructions/local_browser_assets.instructions.md](../instructions/local_browser_assets.instructions.md), and the deterministic checker in [scripts/check_xss_sinks.py](../../scripts/check_xss_sinks.py). Prefer the repo's existing rendering helpers and Bootstrap patterns over new abstractions.

## Operating Rules

- Work in the SimpleChat repository root.
- Treat JavaScript, HTML/Jinja, and Python routes/helpers that send data to the browser as the audit surface.
- On branches that contain the V2 React UI, also treat the TypeScript source in `application/v2_ui/src` (`.ts`, `.tsx`, `.jsx`, `.mjs`) as audit surface. Skip vendored minified libraries under `application/v2_ui/public/vendor/`; the checker skips them too.
- Do not revert unrelated user changes.
- Start with a concise plan and keep it updated as findings are triaged and fixed.
- If the user asks for `scan only` or `stop after plan`, do not edit files.
Expand All @@ -27,15 +28,19 @@ Use the repository guardrails in [.github/instructions/xss-prevention.instructio
2. Identify tracked application files in scope:

```powershell
$xssFiles = git ls-files "application/**/*.js" "application/**/*.html" "application/**/*.py"
$xssFiles = @(git ls-files "application/**/*.js" "application/**/*.html" "application/**/*.py" "application/**/*.ts" "application/**/*.tsx" "application/**/*.jsx" "application/**/*.mjs")
```

3. Run the deterministic checker in full-file mode, saving the output if it is large:
3. Run the deterministic checker in full-file mode, saving the output if it is large. The full file list is longer than the 32,767-character limit Windows puts on a command line, so pass it in batches, and read every batch's output rather than only the last exit code:

```powershell
python scripts/check_xss_sinks.py --full-file $xssFiles
for ($i = 0; $i -lt $xssFiles.Count; $i += 200) {
python scripts/check_xss_sinks.py --full-file $xssFiles[$i..([Math]::Min($i + 199, $xssFiles.Count - 1))]
}
```

TypeScript-family findings start with a rule id such as `[ts-jsx-url-attribute]`. The reviewed V2 baseline, with the reason each finding is safe, is in [docs/explanation/features/v0.241.022/XSS_PR_GUARDRAILS_V2_BASELINE_TRIAGE.md](../../docs/explanation/features/v0.241.022/XSS_PR_GUARDRAILS_V2_BASELINE_TRIAGE.md). Compare against it so already-reviewed sites are not re-triaged from scratch, but re-check any baseline site whose code has changed.

4. Run the guardrail self-test when the checker, workflow, XSS instructions, or this prompt are part of the work:

```powershell
Expand All @@ -54,6 +59,13 @@ rg -n "innerHTML|outerHTML|insertAdjacentHTML|\.html\(|onclick=|onerror=|onload=
rg -n "`[^`]*\$\{|href=.*\$\{|src=.*\$\{|title=.*\$\{|style=.*\$\{|data-[A-Za-z0-9_-]+=.*\$\{" application/single_app/static application/single_app/templates
```

7. When `application/v2_ui/src` exists, search it for React and DOM sinks that bypass JSX escaping:

```powershell
rg -n "dangerouslySetInnerHTML|innerHTML|outerHTML|insertAdjacentHTML|document\.write|createContextualFragment|DOMParser|eval\(|new Function|javascript:|rehype-raw|allowDangerousHtml|urlTransform" application/v2_ui/src
rg -n "(href|src|action|formAction|srcDoc|xlinkHref|to)=\{|window\.open\(|location\.(href|assign|replace)|\.setAttribute\(" application/v2_ui/src
```

If shell quoting gets in the way, use equivalent `rg` searches or the VS Code search tool, but preserve the same coverage.

## Manual Audit Checklist
Expand All @@ -74,6 +86,17 @@ Review these sink categories:
- Python `Markup(...)` and Jinja `|safe` on values that can contain user, model, file, or service-provided content.
- Static HTML shell exceptions that are no longer static because they interpolate runtime data.

For the V2 React UI, also review these sink categories. React 18 escapes JSX text children and attribute strings, but it does not block `javascript:` URLs in URL props (it only warns in development), and it never inspects values that go straight to the DOM:

- `dangerouslySetInnerHTML` whose `__html` is not `DOMPurify.sanitize(...)` output, or whose renderer (KaTeX, Mermaid) runs without its safe mode (`trust: false`, `securityLevel: 'strict'`).
- `innerHTML`/`outerHTML` assignments through refs, `insertAdjacentHTML`, `document.write`, `createContextualFragment`, and `DOMParser` output appended to the page.
- `eval`, `new Function`, and string arguments to `setTimeout`/`setInterval`.
- Dynamic `href`, `src`, `action`, `formAction`, `srcDoc` and `xlinkHref` props, and router `to` props on `Link`, `NavLink` and `Navigate`, that are not a literal, a same-origin path, a reviewed URL builder, a `sanitize*Url`/`normalize*Url` helper, or guarded by `isSafeOriginHref(...)` or an `http(s)` scheme test.
- Navigation sinks: `window.open(...)`, `location.href = ...`, `location = ...`, and `location.assign(...)`/`location.replace(...)` with a dynamic value.
- DOM URL properties such as `element.href`, `element.src`, `form.action`, `frame.srcdoc`, and `setAttribute('href' | 'src' | 'action' | 'srcdoc', ...)`.
- `react-markdown` options that re-enable raw HTML or unfiltered URLs: `rehype-raw`, `allowDangerousHtml`, or a `urlTransform` that returns its input unchanged.
- Props that forward a URL into one of these sinks from another component. The checker sees only the file with the sink, so trace the value back through its callers.

## Triage And Plan

Group findings before editing:
Expand Down Expand Up @@ -105,21 +128,31 @@ Use these fixes by default:
- Keep static modal/card/table shells fully static, then populate untrusted fields with DOM APIs.
- Remove unnecessary `Markup(...)` and Jinja `|safe`; if HTML is required, make the sanitizer boundary explicit and covered by tests.

For V2 React components:

- Render untrusted text as JSX children instead of building HTML strings.
- Keep `dangerouslySetInnerHTML` only for sanitizer output: `{{ __html: DOMPurify.sanitize(html) }}` or a clearly named sanitized `const`.
- Build same-origin links from a literal path and `encodeURIComponent(...)` segments, or use an existing reviewed URL builder. Guard external links with `isSafeOriginHref(...)` or an explicit `http(s)` scheme test before they reach `href`, `window.open`, or `location`.
- Keep `react-markdown` on its defaults: no `rehype-raw`, no `allowDangerousHtml`, and the default URL transform.

Avoid broad mechanical rewrites. Preserve behavior, accessibility, Bootstrap state classes, and local asset references.

## Verification

After each remediation batch, run the narrowest reliable checks first, then broaden before final reporting:

- `node --check <changed-js-file>` for changed JavaScript files.
- `npm --prefix application/v2_ui run typecheck` for changed V2 TypeScript files.
- `python -m py_compile <changed-python-file>` for changed Python files.
- Relevant functional or UI tests for the edited workflow.
- `python scripts/check_xss_sinks.py --full-file <changed-files>` for edited application JS/HTML/Python files.
- Full audit rerun before completion when the user asked for a complete fix pass:
- `python scripts/check_xss_sinks.py --full-file <changed-files>` for edited application JS/HTML/Python/TypeScript files.
- Full audit rerun before completion when the user asked for a complete fix pass, in batches as in Baseline Discovery:

```powershell
$xssFiles = git ls-files "application/**/*.js" "application/**/*.html" "application/**/*.py"
python scripts/check_xss_sinks.py --full-file $xssFiles
$xssFiles = @(git ls-files "application/**/*.js" "application/**/*.html" "application/**/*.py" "application/**/*.ts" "application/**/*.tsx" "application/**/*.jsx" "application/**/*.mjs")
for ($i = 0; $i -lt $xssFiles.Count; $i += 200) {
python scripts/check_xss_sinks.py --full-file $xssFiles[$i..([Math]::Min($i + 199, $xssFiles.Count - 1))]
}
```

- `git -c core.whitespace=blank-at-eol,blank-at-eof,space-before-tab,cr-at-eol diff --check`.
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/broken-access-control-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- Development
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
paths:
- 'application/**/*.py'
- 'scripts/check_broken_access_control.py'
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ name: "CodeQL Advanced"

on:
push:
branches: [ "Development" ]
branches:
- Development
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
pull_request:
branches: [ "Development" ]
branches:
- Development
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
workflow_dispatch:

jobs:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/malicious-pr-security-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- Development
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
paths:
- 'application/**'
- 'deployers/**'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/python-syntax-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:
branches:
- main
- Development
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
paths:
- 'application/single_app/**.py'
- '.github/workflows/python-syntax-check.yml'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/swagger-route-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
- Development
- Staging
- paullizer-react-v2-ui # Temporary: remove when paullizer-react-v2-ui merges into Development (#1571)
paths:
- 'application/single_app/**/*.py'
- 'scripts/check_swagger_routes.py'
Expand Down
Loading
Loading