From 979cbc72d1b26cf2065a1c86bebf02ec0e6c8570 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 07:44:33 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRITICAL]?= =?UTF-8?q?=20Fix=20Path=20Traversal=20in=20Battle.net=20API=20Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit URL-encodes the `realmSlug` and `characterName` variables when constructing paths for the Battle.net API in `getCharacterProfile`, `getCharacterMedia`, and `getCharacterSpecializations`. Because these values are sometimes populated from the `preferences` Firestore collection, which clients have write access to, a malicious user could insert path traversal sequences (e.g. `../`) and trigger SSRF vulnerabilities or manipulate backend API requests. Applying `encodeURIComponent` mitigates this risk. --- .jules/sentinel.md | 4 ++++ packages/functions/src/battlenet.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1ebf946b..71ebbe18 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -18,3 +18,7 @@ **Vulnerability:** `crypto.timingSafeEqual` throws an error if buffers are different lengths, which exposes the application to timing attacks because the error throwing takes a different amount of time than a successful byte-by-byte comparison. **Learning:** Always check buffer lengths before calling `timingSafeEqual`. To ensure constant time regardless of length, compare the expected buffer to itself when lengths don't match. **Prevention:** Compare lengths first, and use a dummy `timingSafeEqual(expected, expected)` on mismatch to mitigate timing leaks. +## 2026-07-18 - [Path Traversal in API Client via Client-Writable Database] +**Vulnerability:** Path traversal / SSRF risk in the Battle.net API client (`packages/functions/src/battlenet.ts`) where `realmSlug` and `characterName` were directly interpolated into URL paths without URL-encoding. +**Learning:** Scheduled backend jobs reading from client-writable databases (like Firestore, where `firestore.rules` allows clients to write to `preferences`) bypass upstream validation that might exist in typical REST API request handlers. Malicious data in the DB could be processed by backend jobs, manipulating downstream API requests. +**Prevention:** Always apply defense-in-depth URL encoding (`encodeURIComponent`) when constructing URLs with data from external sources, even if that data is read from your own database, as the database itself might be populated by untrusted clients. diff --git a/packages/functions/src/battlenet.ts b/packages/functions/src/battlenet.ts index 6556b3fe..65e13427 100644 --- a/packages/functions/src/battlenet.ts +++ b/packages/functions/src/battlenet.ts @@ -52,7 +52,7 @@ export class BattleNetClient { async getCharacterProfile(region: string, realmSlug: string, characterName: string) { const response = await this.apiCall( region, - `/profile/wow/character/${realmSlug}/${characterName.toLowerCase()}?namespace=profile-${region}&locale=en_US`, + `/profile/wow/character/${encodeURIComponent(realmSlug)}/${encodeURIComponent(characterName.toLowerCase())}?namespace=profile-${region}&locale=en_US`, ); if (!response.ok) return null; return response.json(); @@ -61,7 +61,7 @@ export class BattleNetClient { async getCharacterMedia(region: string, realmSlug: string, characterName: string) { const response = await this.apiCall( region, - `/profile/wow/character/${realmSlug}/${characterName.toLowerCase()}/character-media?namespace=profile-${region}&locale=en_US`, + `/profile/wow/character/${encodeURIComponent(realmSlug)}/${encodeURIComponent(characterName.toLowerCase())}/character-media?namespace=profile-${region}&locale=en_US`, ); if (!response.ok) return null; return response.json(); @@ -70,7 +70,7 @@ export class BattleNetClient { async getCharacterSpecializations(region: string, realmSlug: string, characterName: string) { const response = await this.apiCall( region, - `/profile/wow/character/${realmSlug}/${characterName.toLowerCase()}/specializations?namespace=profile-${region}&locale=en_US`, + `/profile/wow/character/${encodeURIComponent(realmSlug)}/${encodeURIComponent(characterName.toLowerCase())}/specializations?namespace=profile-${region}&locale=en_US`, ); if (!response.ok) return null; return response.json();