Skip to content

security: fix Solid server auto-trust hostname matching#17

Closed
jjohare wants to merge 1 commit into
JavaScriptSolidServer:mainfrom
jjohare:security/fix-solid-server-matching
Closed

security: fix Solid server auto-trust hostname matching#17
jjohare wants to merge 1 commit into
JavaScriptSolidServer:mainfrom
jjohare:security/fix-solid-server-matching

Conversation

@jjohare

@jjohare jjohare commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Severity: HIGH-03 (auto-trust bypass via substring matching)
  • Replace String.includes() substring matching in isLikelySolidServer() with exact hostname matching via new URL().hostname
  • The previous code allowed any domain containing a trusted substring to be auto-trusted (e.g. evil-solid.social.attacker.com matched solid.social, not-solidcommunity.net.evil.com matched solidcommunity.net)
  • Now performs exact hostname match or legitimate subdomain match (hostname === trusted || hostname.endsWith('.' + trusted))
  • Removes the '/.well-known/solid' indicator which never matched since origins don't contain paths
  • Invalid origins that fail URL parsing return false instead of potentially throwing

Test plan

  • Verify https://solid.social still matches as a Solid server
  • Verify https://pod.solid.social still matches (subdomain)
  • Verify https://evil-solid.social.attacker.com does NOT match
  • Verify https://not-solidcommunity.net.evil.com does NOT match
  • Verify malformed origins (non-URL strings) return false without throwing

Co-Authored-By: claude-flow ruv@ruv.net

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/solid path indicator.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@jjohare

jjohare commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator Author

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.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


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.

jjohare added a commit that referenced this pull request Jun 15, 2026
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
@jjohare

jjohare commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #22 (merged as 384918d), which consolidates this branch's fixes into one reviewed, CI-green change. Closing in favour of that.

@jjohare jjohare closed this Jun 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants