diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 57c2298..ea4e8c3 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -2,3 +2,8 @@ **Vulnerability:** Missing input length limits on user-provided strings (Player Name) could lead to UI breakage or minor DoS. **Learning:** Enforcing limits at both the UI layer (maxLength) and the System layer (trim/slice) provides defense-in-depth and ensures data integrity regardless of the entry point. **Prevention:** Always apply length constraints and sanitization to user-controlled inputs that are persisted or rendered globally. + +## 2025-05-16 - Structural Validation for Deserialized Data +**Vulnerability:** Insecure deserialization of save game data from `localStorage` using blind type casting (`as SaveData`). +**Learning:** Blindly trusting data from `localStorage` or other external sources can lead to runtime crashes or logic errors if the data is malformed or maliciously modified. +**Prevention:** Always perform structural validation (checking presence and types of required fields) on deserialized JSON data before using it in application logic. diff --git a/src/game/systems/SaveSystem.ts b/src/game/systems/SaveSystem.ts index e3044ac..c7d7c11 100644 --- a/src/game/systems/SaveSystem.ts +++ b/src/game/systems/SaveSystem.ts @@ -34,12 +34,33 @@ export class SaveSystem { static deserialize(serialized: string): SaveData | null { try { - return JSON.parse(serialized, (key, value) => this.reviver(key, value)) as SaveData; + const parsed = JSON.parse(serialized, (key, value) => this.reviver(key, value)) as unknown; + if (this.isValidSaveData(parsed)) { + return parsed; + } + return null; } catch { return null; } } + private static isValidSaveData(data: unknown): data is SaveData { + if (typeof data !== 'object' || data === null) { + return false; + } + + const candidate = data as Record; + return ( + Array.isArray(candidate.players) && + typeof candidate.currentPlayerId === 'string' && + typeof candidate.turn === 'number' && + typeof candidate.phase === 'string' && + candidate.europePrices !== null && + typeof candidate.europePrices === 'object' && + Array.isArray(candidate.map) + ); + } + private static replacer(_key: string, value: unknown): unknown { if (value instanceof Map) { return {