Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
6 changes: 3 additions & 3 deletions packages/functions/src/battlenet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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();
Expand Down
Loading