@mrzr/api-client

Security

What @mrzr/api-client protects against and what it doesn't: token isolation, trusted origins, storage trade-offs, recommended setups and a CSP.

What it protects against

ThreatProtection
Token theft through XSSWorker isolation plus "memory" storage: the token has no variable or storage key page code can reach
Token sent to the wrong serverThe token and CSRF header only go to the baseUrl origin, the page's origin and authOrigins. Each URL is parsed once, and that exact URL is both checked and fetched, so tricks like //evil.com or \\evil.com get no credentials
Token read by page codegetAccessToken() refuses unless exposeTokens: true. Responses leaving the worker have the session's tokens removed
Path injectionaddTemplateToUrl and addToUrl values are encoded as one path segment
CSRFThe double-submit header in cookie mode, if your server checks it
Tokens in logsNever in LogEntry, AuthState or messages between tabs
Refresh token reuseOne refresh at a time, per tab and across tabs
A malicious dependencyThere are none: zero runtime dependencies

It doesn't address man-in-the-middle attacks: use HTTPS.

What it can't do: XSS can still use the session

Worker isolation stops the token from being stolen. It can't stop injected script from using your client: api.post("/transfer", …) from an attacker gets the token attached like any other call.

That still changes a lot:

Without isolationWith isolation
The token is sent to the attacker's serverThe token stays in the worker
The attack continues after the tab closesThe attack ends with the page
A stolen refresh token gives indefinite accessThe refresh token never leaves

It's defence in depth. Preventing XSS (a strict CSP, output encoding, careful dependencies) still comes first.

Endpoints of yours that hand out a new credential (an OAuth token exchange, an API key endpoint, a socket ticket) return it to whoever calls them, including injected script. Keep those credentials short-lived and narrowly scoped.

Storage, from safest to least safe

  1. authMode: "cookie" with httpOnly cookies: the client never sees a token.
  2. Header mode, storage: "memory", worker on: the token can't be reached from the page.
  3. Header mode, "memory", worker off: the token sits in a closure on the page.
  4. "session": readable by any script on the page.
  5. "local": readable, and persists.
  6. "cookie" (not httpOnly): readable, and sent with every request to your site.

The trade-off is reloads: "memory" logs the user out on reload, cookie mode doesn't.

You control the backend and share a site with it. The strongest option:

createClient({ baseUrl: "https://api.example.com", authMode: "cookie", xsrfCookieName: "csrftoken" });
Set-Cookie: session=…; HttpOnly; Secure; SameSite=Strict; Path=/
Set-Cookie: csrftoken=…; Secure; SameSite=Strict; Path=/

A SPA calling someone else's API. Header mode with the defaults:

createClient({ baseUrl: "https://api.vendor.com", onAuthFailure: () => router.push("/login") });

Sessions must survive reloads and cookies aren't an option. Accept the XSS exposure and add a strict CSP. The worker is still worth keeping, since the live token stays inside it:

createClient({ baseUrl, storage: "local" });

Content Security Policy

Content-Security-Policy:
  default-src 'self';
  script-src 'self';
  worker-src 'self' blob:;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'self';
  frame-ancestors 'none';
  • worker-src blob: is required for the worker. Without it the client falls back to the main thread without an error.
  • connect-src limits where fetch can go. It's what actually stops data being sent to an attacker's domain.
  • Avoid 'unsafe-inline' and 'unsafe-eval' in script-src: they're what make XSS easy.

Checklist

  • api.isWorker is true in production browsers, or worker: false is deliberate
  • storage is "memory", or you've accepted the exposure
  • Cookie mode has CSRF configured, and the server enforces it
  • The CSP has worker-src blob: and a tight connect-src
  • baseUrl is HTTPS in production
  • onAuthFailure clears cached user data, not just the route
  • Access tokens are short-lived (15 minutes or less), and refresh tokens rotate
  • No token appears in analytics or error reports

Report vulnerabilities through a security advisory, not a public issue.

On this page