T09 · Insecure Skill Coding Practices
Error
- Location
- references/react-spa.md:69
- Finding
- Bearer Token Can Be Sent to an Unrestricted Network Destination## Vulnerability Details **File Location**: `references/react-spa.md`, lines 69–75 **Vulnerability Type**: Unrestricted bearer-token destination **Risk Level**: High ### Vulnerable Code ```typescript export async function apiFetch(auth: { user?: { access_token?: string } }, input: string) { return fetch(input, { headers: auth.user?.access_token ? { Authorization: `Bearer ${auth.user.access_token}` } : undefined, }); } ``` ### Technical Analysis The documented API helper accepts an unrestricted string as its request destination and automatically adds the current OIDC access token as an `Authorization` bearer credential. It does not require a relative API path, constrain requests to a configured first-party API origin, validate the destination against an allowlist, or reject unsafe schemes and untrusted hosts. Attaching an access token to requests sent to the application's trusted API is necessary for the Skill's declared OIDC functionality. Allowing that credential to be attached to an arbitrary caller-provided destination exceeds the minimum network privilege necessary. If an application adopts this example and any attacker-controlled value can reach `input`, the helper can transmit the token to an attacker-controlled endpoint. Although browser CORS rules can restrict access to responses, they do not generally prevent the outbound authenticated request or the receiving server from observing its `Authorization` header. ### Attack Path 1. A developer adopts the documented `apiFetch` helper in a React or TypeScript SPA. 2. A URL derived from untrusted input, compromised application state, a malicious link, or another attacker-influenced source is passed to `input`. 3. The attacker supplies a destination such as `https://attacker.example/collect`. 4. The helper adds `Authorization: Bearer <access_token>` and sends the request to that destination. 5. The attacker-controlled server record ...[truncated 864 chars]
- Remediation
- ## Remediation Suggestions Replace the unrestricted authenticated fetch helper with one bound to an explicitly configured and trusted API origin: 1. Accept relative API paths rather than arbitrary absolute URLs. 2. Resolve each path against a fixed `VITE_API_BASE_URL`. 3. Verify that the resolved URL's origin exactly matches the configured API origin before adding the bearer token. 4. Require HTTPS for non-development deployments. 5. Keep authenticated API requests separate from a generic unauthenticated fetch helper. 6. Avoid accepting redirect modes or request options that could forward credentials unexpectedly. 7. Continue enforcing issuer, audience, scope, and expiry checks at the resource server; client-side destination checks are defense in depth and do not replace server-side validation. Example hardened pattern: ```typescript const apiBaseUrl = new URL(import.meta.env.VITE_API_BASE_URL); export async function apiFetch( auth: { user?: { access_token?: string } }, path: string, ) { const url = new URL(path, apiBaseUrl); if (url.origin !== apiBaseUrl.origin) { throw new Error('Refusing to send credentials to an untrusted origin'); } if (import.meta.env.PROD && url.protocol !== 'https:') { throw new Error('Authenticated API requests must use HTTPS'); } return fetch(url, { headers: auth.user?.access_token ? { Authorization: `Bearer ${auth.user.access_token}` } : undefined, redirect: 'error', }); } ``` For stricter enforcement, reject absolute input altogether and permit only paths beginning with `/`. Document that authenticated requests must only target the configured resource server.
