T09 · Insecure Skill Coding Practices
Error
- Location
- scripts/bring_list.js:373
- Finding
- Authenticated Bring Headers Can Be Exfiltrated to an Arbitrary Host<![CDATA[ ## Vulnerability Details **File Location**: `scripts/bring_list.js:46-54, 373-381` **Vulnerability Type**: Arbitrary destination request with sensitive authentication headers **Risk Level**: High ### Vulnerable Code ```js async function fetchContent(url, headers) { const resp = await fetch(url, { headers }); const text = await resp.text(); try { return JSON.parse(text); } catch (err) { throw new Error(`Non-JSON response (${resp.status}): ${text.slice(0, 200)}`); } } ``` ```js if (contentUrlRaw) { const urls = contentUrlRaw .split(",") .map((s) => s.trim()) .filter(Boolean); const allItems = []; for (const url of urls) { const content = await fetchContent(url, bring.headers); const items = extractItemsFromContent(content); allItems.push(...items); } ``` The sensitivity of `bring.headers` is confirmed by `references/bring-inspirations.md:12-18`: ```md 2. Set headers for subsequent calls: - `X-BRING-API-KEY`: `<public client key from bring-shopping npm package>` - `X-BRING-CLIENT`: `webApp` - `X-BRING-CLIENT-SOURCE`: `webApp` - `X-BRING-COUNTRY`: `DE` (use user locale country if known) - `X-BRING-USER-UUID`: `<uuid from login>` - `Authorization`: `Bearer <access_token>` ``` ### Technical Analysis The `--content-url` option accepts a caller-controlled, comma-separated collection of URLs. The code does not validate the URL scheme, hostname, port, or path before passing each URL to `fetchContent`. After the Skill logs into the Bring service, it passes the complete `bring.headers` object to `fetch`. According to the bundled API reference, that object contains an OAuth-style bearer access token and the user's Bring UUID. Consequently, a direct URL pointing to an attacker-controlled server receives headers intended only for the Bring API. The declared functionality only requires authenticated requests to Bring template endpoints such as: ```text https://api.getbring.com/rest/v2/bring ...[truncated 1942 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Parse every supplied URL using `new URL()` and reject malformed values. 2. Require the `https:` scheme. 3. Allow only the expected hostname: ```js parsed.hostname === "api.getbring.com" ``` 4. Require the expected path prefix: ```text /rest/v2/bringtemplates/content/ ``` 5. Reject embedded credentials, non-default ports, IP literals, fragments, and unexpected query parameters. 6. Disable redirects with `redirect: "error"` so an approved Bring URL cannot redirect the request to another origin. 7. Construct an explicit minimum header allowlist rather than passing the complete mutable `bring.headers` object. 8. Prefer accepting a Bring content UUID rather than a full URL, then construct the trusted URL internally. 9. Apply request timeouts and response-size limits to reduce denial-of-service exposure. 10. Add tests proving that external domains, HTTP URLs, alternate ports, loopback addresses, private addresses, and redirects are rejected. A safer design would resemble: ```js function buildContentUrl(contentUuid) { if (!/^[0-9a-f-]+$/i.test(contentUuid)) { throw new Error("Invalid Bring content UUID."); } return new URL( `/rest/v2/bringtemplates/content/${contentUuid}`, "https://api.getbring.com" ); } async function fetchBringContent(contentUuid, bringHeaders) { const url = buildContentUrl(contentUuid); const response = await fetch(url, { headers: { Authorization: bringHeaders.Authorization, "X-BRING-API-KEY": bringHeaders["X-BRING-API-KEY"], "X-BRING-CLIENT": bringHeaders["X-BRING-CLIENT"], "X-BRING-CLIENT-SOURCE": bringHeaders["X-BRING-CLIENT-SOURCE"], "X-BRING-COUNTRY": bringHeaders["X-BRING-COUNTRY"], "X-BRING-USER-UUID": bringHeaders["X-BRING-USER-UUID"], }, redirect: "error", }); if (!response.ok) { throw new Error(`Bring content request failed: ${response.status}`); } return response.json(); } ``` ]]>
