docs(security): add 2026-10-12 audit log; refresh NIP-05 and popup rendering notes
Summarises the code review + dependency scan: no exploitable client/server issue found; documents why npm audit fix was reverted (broke the build) and which advisories are unreachable/accepted (xlsx export-only, HashRouter open-redirect, MCP client-side, build-tool deps). Fixes the outdated 'never innerHTML' claim (popups use innerHTML with escapeHtml) and updates the contact NIP-05 to the seeded skatespots@skate-spots.org.
This commit is contained in:
parent
ca810326f3
commit
4674303f3b
1 changed files with 61 additions and 5 deletions
66
SECURITY.md
66
SECURITY.md
|
|
@ -18,7 +18,7 @@ Please report privately first — **do not** open a public issue or publish a Po
|
||||||
before we've had a chance to respond.
|
before we've had a chance to respond.
|
||||||
|
|
||||||
- **Nostr DM (preferred):** `npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx`
|
- **Nostr DM (preferred):** `npub1yrnuj56rnen08zp2h9h7p74ghgjx6ma39spmpj6w9hzxywutevsst7k5cx`
|
||||||
- **NIP-05:** `skatespots@goskatespots.nostr1.com`
|
- **NIP-05:** `skatespots@skate-spots.org` (seeded platform handle)
|
||||||
|
|
||||||
Please include:
|
Please include:
|
||||||
|
|
||||||
|
|
@ -178,10 +178,10 @@ Additional headers: `X-Content-Type-Options: nosniff`,
|
||||||
- **React/JSX** auto-escapes all interpolated strings — the default rendering
|
- **React/JSX** auto-escapes all interpolated strings — the default rendering
|
||||||
path. Do not bypass it.
|
path. Do not bypass it.
|
||||||
- **Leaflet map popups & marker icons** (`src/components/SpotMap.tsx`) are the
|
- **Leaflet map popups & marker icons** (`src/components/SpotMap.tsx`) are the
|
||||||
one place we build DOM by hand. Every untrusted field (spot/tour/crew/event
|
one place we build DOM by hand. Popups set `innerHTML` from a template in which
|
||||||
titles & locations, meeting points, BTC Map name/address/hours/phone/
|
**every dynamic value is passed through `escapeHtml`** (re-verified 2026-10-12);
|
||||||
description) is written via `textContent`, never `innerHTML`. A `popupRow()`
|
marker icons and the weather card use `textContent`. Image/website fields are
|
||||||
helper enforces this. Image/website fields are validated before use.
|
validated before use.
|
||||||
- Rich Nostr note content is rendered through the shared `NoteContent`
|
- Rich Nostr note content is rendered through the shared `NoteContent`
|
||||||
component/tokenizer rather than raw HTML.
|
component/tokenizer rather than raw HTML.
|
||||||
|
|
||||||
|
|
@ -294,6 +294,62 @@ stronger handling than other secrets:
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Audit log — 2026-10-12 (code review + dependency scan)
|
||||||
|
|
||||||
|
A targeted review of the client (`src/`), the standalone servers (`cvm-server`,
|
||||||
|
`push-service`, `ai-proxy`, `netlify/functions`) and the dependency tree.
|
||||||
|
|
||||||
|
**No exploitable issue was found in app code.**
|
||||||
|
|
||||||
|
- **Injection / XSS:** no `eval` / `new Function`; `dangerouslySetInnerHTML`
|
||||||
|
appears only in shadcn's `chart.tsx` (static, developer-defined chart CSS). The
|
||||||
|
hand-built Leaflet popup HTML interpolates values but every dynamic value is
|
||||||
|
`escapeHtml`'d (verified). `document.write` (recovery-phrase QR and rider-card
|
||||||
|
print windows) interpolates only canvas data-URLs and i18n strings. No
|
||||||
|
`__proto__` / prototype pollution. No secrets in web storage beyond the
|
||||||
|
documented `nsec`/NWC/Cashu.
|
||||||
|
- **Reverse tabnabbing:** every `target="_blank"` also carries
|
||||||
|
`rel="noopener noreferrer"`.
|
||||||
|
- **SSRF:** the only `fetch` to a derived host is
|
||||||
|
`https://<domain>/.well-known/lnurlp/…` where the domain comes from a
|
||||||
|
**hardcoded platform Lightning address** (not user input). Servers do not fetch
|
||||||
|
user-supplied URLs. The push service additionally validates subscription
|
||||||
|
endpoints (`isSafePushEndpoint`: https-only; blocks localhost + private /
|
||||||
|
link-local / CGNAT ranges).
|
||||||
|
- **Auth:** NIP-98 (kind 27235) signature + timestamp checks gate push
|
||||||
|
subscribe/unregister and the AI proxy; zap receipts and owner-grant labels are
|
||||||
|
signature-verified (`verifyEvent`) on both client and servers; the CVM server
|
||||||
|
signature-verifies event content before serving it to agents.
|
||||||
|
- **CORS:** the AI proxy and push service enforce origin allowlists; the NIP-05
|
||||||
|
well-known function is intentionally `Access-Control-Allow-Origin: *` (public).
|
||||||
|
- **Randomness:** `Math.random` is used only for UI ids, probe/subscription ids,
|
||||||
|
PoW nonces and privacy timestamp jitter — never for secrets or tokens.
|
||||||
|
|
||||||
|
**Dependencies (`npm audit`).** `npm audit fix` updated `@remix-run/router` (open
|
||||||
|
redirect), `@modelcontextprotocol/sdk`, `@humanfs/node`, `valibot` and
|
||||||
|
`brace-expansion`, but was **reverted** because it re-resolved the lockfile and
|
||||||
|
broke the build (a nested `@nostrify/nostrify` duplicate introduced a type
|
||||||
|
conflict). Re-assessment: none of the remaining advisories is reachable from the
|
||||||
|
shipped app —
|
||||||
|
|
||||||
|
- `xlsx` (high, prototype pollution + ReDoS, no fix): used only to **generate** the
|
||||||
|
deck-cost workbook (`XLSX.writeFile`); imported files are never **parsed**, so
|
||||||
|
the parsing flaws are unreachable. Accepted.
|
||||||
|
- `@remix-run/router` open redirect: unreachable — the app uses **HashRouter**, so
|
||||||
|
navigation stays inside the `#` fragment and never crosses to an external URL.
|
||||||
|
- `@modelcontextprotocol/sdk` OAuth flaw: affects MCP **clients** connecting to
|
||||||
|
untrusted servers; `cvm-server` is an MCP **server**, not a client. Not
|
||||||
|
applicable.
|
||||||
|
- `braces`, `@vitest/mocker`, `@capacitor/cli`'s `uuid`, `tailwindcss`: build/test
|
||||||
|
tooling only — no runtime reach into the served app. Accepted.
|
||||||
|
|
||||||
|
A broken build is a larger risk than an unreachable CVE, so the broken fix was
|
||||||
|
reverted rather than forced through.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## For auditors: where to look first
|
## For auditors: where to look first
|
||||||
|
|
||||||
| Area | Files |
|
| Area | Files |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue