security: fix Solid server auto-trust hostname matching#17
Conversation
Replace String.includes() with exact hostname matching via URL parsing.
The previous substring check allowed malicious domains containing the
trusted substring to pass (e.g. evil-solid.social.attacker.com matched
solid.social). Now uses new URL().hostname with exact match or subdomain
check (hostname.endsWith('.' + trusted)). Also removes the never-matching
'/.well-known/solid' entry since origins don't contain paths.
Co-Authored-By: claude-flow <ruv@ruv.net>
There was a problem hiding this comment.
Pull request overview
This PR tightens Solid server auto-trust detection by replacing substring checks with URL hostname parsing and exact/subdomain matching, preventing attacker-controlled domains from matching trusted host substrings.
Changes:
- Parses origins with
new URL()and safely rejects malformed values. - Replaces broad
includes()matching with trusted hostname equality or legitimate subdomain matching. - Removes the ineffective
/.well-known/solidpath indicator.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Folded into #22 — a single CI-green, adversarially-reviewed PR that consolidates this branch with the other security fixes plus NIP-44 and residual gap-fixes (exact-host trust, NIP-98 nonce, signEvent self-verify, honest provider surface, auto-sign default OFF). Suggest reviewing/merging #22; this can be closed in its favour. |
|
DreamLab-AI Mega-Sprint seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Consolidated hardening of the Podkey key-holding signer: brings seven in-flight security/feature branches plus residual gap-fills, a polished CSP-clean UI, a CI build/test/lint gate, and a 133-test suite into one reviewable PR. The fix set was independently re-derived from a full audit of the extension and every item below was verified fixed *in the final tree* by an adversarial review pass (not just claimed). **Supersedes** (folded in here; safe to close in favour of this PR): #12, #14, #17, #18, #19, #20, #21. ## Security posture — before → after | Area | Before | After | |---|---|---| | Consent | Auto-approve everything (`showPermissionPrompt` hard-returned `true`) | **Explicit per-origin consent** popup that blocks on the user's decision; closing or 60s timeout denies | | Key at rest | `storage.local` (persisted plaintext) | **`storage.session` only** (in-memory, cleared on browser close); raw key never crosses to the page | | Crypto | A fake `crypto-browser.js` in the tree (`pubkey = sha256(privkey)`) | Deleted; **real `@noble` crypto** only, with `signEvent` schnorr self-verify before return | | NIP-98 | Signed-event cache → byte-identical id → replayable | **Fresh per-token 16-byte nonce**, redirect-aware `u` tag, page-side body-hash binding | | Solid-host trust | Substring match (`inrupt.net.evil.com` accepted) | **Exact-host match**, case-insensitive; lookalikes rejected | | Interception | Double fetch/XHR interception + double 401-retry | **Single interception path**, one retry | | Message channel | Page could inject arbitrary fields / origin / `privateKey` | Content script **whitelists 4 message types, forwards only safe fields, enforces the real origin** | | Provider surface | Advertised `nip04`/`getRelays` that throw / return `{}` | **Honest**: `getPublicKey`, `signEvent`, `nip44.{encrypt,decrypt}` only | | UI | Inline scripts + `innerHTML` XSS sinks | **CSP `script-src 'self'`**; all DOM via `textContent`/`createElement` | | Auto-sign | Defaulted **ON** (first-run silent trust + NIP-98 mint) | Defaults **OFF** — silent trusted-origin Solid signing and NIP-98 auto-trust are strictly opt-in | ## Notes for review - **Consent timeout:** `approve.js` actively sends `approved:false` + closes at the 60s mark (one-shot `decisionSent` guard) so the visible countdown is truthful; the background `chrome.windows` 60s timer remains a backstop. If you'd prefer the popup stay passive and let the background own the timeout, that is the single behavioural line to reconsider — everything else on the UI is restyling. - The page-context `nip98-interceptor.js` still duplicates a couple of `auth-header-utils.js` helpers because page context can't ESM-import the module; intentional, tracked separately. ## Verification - `npm run build` ✓ · `npm test` → **133 pass / 0 fail** ✓ · `npm run lint` → **0 errors** (now `no-unused-vars: error`) ✓ - New `.github/workflows/ci.yml` runs build → test → lint on every PR and push to `main`. 🤖 Generated by Claude Code
Summary
String.includes()substring matching inisLikelySolidServer()with exact hostname matching vianew URL().hostnameevil-solid.social.attacker.commatchedsolid.social,not-solidcommunity.net.evil.commatchedsolidcommunity.net)hostname === trusted || hostname.endsWith('.' + trusted))'/.well-known/solid'indicator which never matched since origins don't contain pathsfalseinstead of potentially throwingTest plan
https://solid.socialstill matches as a Solid serverhttps://pod.solid.socialstill matches (subdomain)https://evil-solid.social.attacker.comdoes NOT matchhttps://not-solidcommunity.net.evil.comdoes NOT matchCo-Authored-By: claude-flow ruv@ruv.net