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
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
23 changes: 22 additions & 1 deletion src/game/systems/SaveSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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 {
Expand Down
Loading