T09 · Insecure Skill Coding Practices
- Location
- src/lib/api.ts:23
- Finding
- Untrusted Pagination URL Can Trigger Authenticated Requests to an Unapproved Origin<![CDATA[ ## Vulnerability Details **File Location**: `src/lib/api.ts:23-47` and `src/lib/api.ts:105-110` **Vulnerability Type**: Unvalidated cross-origin pagination URL **Risk Level**: Medium ### Vulnerable Code ```ts function parseNextLink(linkHeader: string | undefined): string | null { if (!linkHeader) return null; const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/); return match ? match[1] : null; } /** * Fetch all pages of a paginated API endpoint and aggregate results * Handles RFC5988 Link header pagination automatically */ async function fetchAllPages<T>( client: Got, url: string, options?: Options ): Promise<T[]> { const allResults: T[] = []; let nextUrl: string | null = url; while (nextUrl) { const response = await client.get(nextUrl, { ...options, responseType: 'json' }); const items = response.body as T[]; allResults.push(...items); const linkHeader = response.headers.link as string | undefined; nextUrl = parseNextLink(linkHeader); } return allResults; } ``` The client used by this function is configured with an OAuth bearer token: ```ts return got.extend({ prefixUrl: `https://3.basecampapi.com/${accountId}/`, headers: { 'Authorization': `Bearer ${accessToken}`, 'User-Agent': USER_AGENT, 'Content-Type': 'application/json' }, ``` ### Technical Analysis The pagination implementation extracts the next-page URL directly from the server-provided HTTP `Link` header and passes it to the authenticated HTTP client without validating its scheme, hostname, port, or origin. An absolute URL such as `https://attacker.example/next` can therefore cause the application to make an unintended outbound request. Because the `Got` client is configured globally with the Basecamp OAuth bearer token, this design also creates a credential-forwarding risk. Whether the authorization header is retained on a cross-origin absolute request depends on the HTTP client's precise header-handling beha ...[truncated 2350 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse each pagination target with the standard `URL` API and resolve relative links against the fixed Basecamp API origin. 2. Require the URL protocol to be exactly `https:`. 3. Require the URL origin to be exactly `https://3.basecampapi.com`; reject alternate hostnames, ports, embedded credentials, and non-HTTPS schemes. 4. Verify that the account path remains within the selected Basecamp account, for example by requiring the pathname to begin with `/${accountId}/`. 5. Avoid using an absolute untrusted URL with a client that has a default authorization header. Prefer extracting and validating the relative path before issuing the next request. 6. Add tests covering malicious pagination links, including: - Cross-origin HTTPS URLs - HTTP URLs - URLs with alternate ports - URLs containing user-information components - Protocol-relative URLs - Redirects from an approved URL to an unapproved origin 7. Configure redirect handling so authorization headers cannot be forwarded to another origin, and reject cross-origin redirects explicitly. Example hardening approach: ```ts const BASECAMP_ORIGIN = 'https://3.basecampapi.com'; function validateNextLink( linkHeader: string | undefined, accountId: number ): string | null { if (!linkHeader) return null; const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/); if (!match) return null; const parsed = new URL(match[1], `${BASECAMP_ORIGIN}/${accountId}/`); if (parsed.protocol !== 'https:' || parsed.origin !== BASECAMP_ORIGIN) { throw new Error('Rejected cross-origin pagination URL'); } if (!parsed.pathname.startsWith(`/${accountId}/`)) { throw new Error('Rejected pagination URL outside the selected account'); } return `${parsed.pathname}${parsed.search}`; } ``` The validated relative path should then be passed to the Basecamp client, ensuring that the bearer token is only used for the intended API origin. ]]>
