T09 · Insecure Skill Coding Practices
Error
- Location
- openclaw-soundcloud-watcher/soundcloud_watcher.ts:403
- Finding
- OAuth Access Token Can Be Forwarded to an Untrusted Pagination URL<![CDATA[ ## Vulnerability Details **File Location**: `openclaw-soundcloud-watcher/soundcloud_watcher.ts`, lines 403-420 and 505-509 **Vulnerability Type**: Unvalidated redirect target causing OAuth credential disclosure **Risk Level**: High ### Vulnerable Code ```ts let fullUrl: string; if (url.startsWith("/")) fullUrl = `${API_BASE}${url}`; else if (url.startsWith("http")) fullUrl = url; else fullUrl = `${API_BASE}/${url}`; if (params) { const sep = fullUrl.includes("?") ? "&" : "?"; const query = new URLSearchParams( Object.fromEntries( Object.entries(params).map(([k, v]) => [k, String(v)]) ) ).toString(); fullUrl = `${fullUrl}${sep}${query}`; } const headers: Record<string, string> = {}; if (this.config.accessToken) { headers["Authorization"] = `OAuth ${this.config.accessToken}`; } try { const resp = await fetch(fullUrl, { headers, signal: AbortSignal.timeout(API_TIMEOUT_MS), }); ``` The remotely supplied pagination URL reaches the method above through this code: ```ts const nextHref = data.next_href; if (nextHref && nextHref !== nextUrl) { nextUrl = nextHref; params = undefined; } else { break; } ``` ### Technical Analysis The API client accepts any string beginning with `http` as a complete request URL. It then attaches the SoundCloud OAuth access token to that request without checking the URL's scheme, hostname, port, or origin. The follower pagination logic obtains `next_href` from a remote API response and passes it back into this generic request method. Consequently, the trust decision about where an authenticated request may be sent is indirectly controlled by remote response data. This violates the security requirement that bearer-style credentials must only be transmitted to explicitly trusted origins. The check also accepts both `http://` and `https://` strings because it only tests `startsWith("http")`; therefore, a supplied HTTP URL could additionally expose the token in plaintext over the netw ...[truncated 1951 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse every outbound URL with `new URL()` rather than relying on string-prefix checks. 2. Require the `https:` protocol. 3. Maintain an explicit allowlist of SoundCloud API hosts, preferably only `api.soundcloud.com` unless additional official hosts are strictly necessary. 4. Reject user- or response-derived URLs containing unexpected credentials, ports, hosts, or protocols. 5. Attach the OAuth header only after confirming that the final destination is an approved SoundCloud origin. 6. Validate `next_href` before assigning or following it. 7. Consider accepting only relative pagination paths and reconstructing the URL against the fixed API base. 8. Disable automatic redirect following or validate every redirect destination, because an approved URL could otherwise redirect an authenticated request to another origin. 9. Add tests proving that tokens are not sent to: - Arbitrary HTTPS hosts - Plain HTTP URLs - Lookalike SoundCloud domains - URLs containing crafted host syntax - Cross-origin redirect destinations A hardened approach should resemble: ```ts private buildTrustedUrl(input: string): URL { const parsed = input.startsWith("/") ? new URL(input, API_BASE) : new URL(input); if ( parsed.protocol !== "https:" || parsed.hostname !== "api.soundcloud.com" || parsed.port !== "" ) { throw new Error("Rejected untrusted SoundCloud API URL"); } return parsed; } ``` The authorization header should only be created after `buildTrustedUrl()` succeeds. Redirects should be disabled with `redirect: "manual"` unless each destination is independently revalidated. ]]>
