T09 · Insecure Skill Coding Practices
- Location
src/lib/api.ts:23- Finding
Untrusted Pagination URL Can Trigger Authenticated Requests to an Unapproved Origin
- Content
View full analysis
]+)>;\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( client: Got, url: string, options?: Options ): Promise { 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
View remediation
]+)>;\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. ]]>
