You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(parity): enforce fail-hard cross-engine parity and sync security docs
- tests/unit/parity.test.ts: Python engine invocation now throws on
execution or parse failure instead of silently passing; shared
assertCrossEngineParity() helper deduplicates both fixtures;
REPODNA_ALLOW_PYTHON_PARITY_SKIP=1 remains a local-only opt-out.
- ci.yml: add dedicated fail-hard parity job (Node 22 + Python 3.12,
pip install -e ., vitest parity suite); visualizer unit tests set the
local skip env since that job has no Python runtime.
- README.md: limits table split into browser/private vs public durable
columns; compression-ratio row now documents the actual
quarantine-and-continue behavior; added cross-engine parity
enforcement section.
- SECURITY.md + docs/threat-model.md: replace stale abort/413
SUSPICIOUS_COMPRESSION_RATIO wording with the implemented quarantine
semantics and public durable limits. No behavior changes.
|**Public durable compressed archive**| 128 MB before Git tree fallback | Uses bounded archive or Git tree acquisition |
108
-
|**Public durable extracted content**| 192 MB | Produces an honest partial inventory when bounded limits are reached |
109
-
|**Interactive graph**| 8,000 nodes / 12,000 edges in the v2 artifact; 240 rendered nodes / edges in the live canvas | Preserves full inventory and reports compaction diagnostics |
110
-
|**GitHub fetch timeout**| 20 seconds private/browser; 60 seconds public durable | Returns a controlled upstream timeout/failure |
97
+
| Limit | Browser / private analysis | Public durable analysis | Action upon breach |
98
+
|---|---|---|---|
99
+
|**Public rate limit**| 5 analyses / 10 min per IP | Same | Returns `429 RATE_LIMITED`|
100
+
|**Authenticated rate limit**| 20 analyses / 10 min per user | Same | Returns `429 RATE_LIMITED`|
|**Individual file size**| 1 MB (1,000,000 bytes) | Same | Skipped with diagnostic (`exceeds_file_size_limit`) |
104
+
|**Declared compression ratio**| 200:1 (entries emitting > 256 KB decompressed) | Same | Entry is **quarantined, not fatal**: its stream is terminated and buffers discarded, it is skipped with a `suspicious_compression_ratio` diagnostic, and analysis of the remaining safe files continues. Every emitted byte—including quarantined entries—still counts toward the cumulative extracted-bytes cap. |
105
+
|**Compressed archive download**| 25 MB (26,214,400 bytes) | 128 MB before Git-tree fallback | Browser/private returns `413 ARCHIVE_TOO_LARGE`; public durable uses bounded archive or Git-tree acquisition |
106
+
|**Extracted content**| 100 MB (104,857,600 bytes) | 192 MB | Browser/private returns `413 EXTRACTED_TOO_LARGE` (ZIP bomb protection); public durable produces an honest partial inventory when bounded limits are reached |
107
+
|**Interactive graph**| 8,000 nodes / 12,000 edges in the v2 artifact; 240 rendered nodes / edges in the live canvas | Same | Preserves full inventory and reports compaction diagnostics |
- Bounded download sizes with streaming network byte caps (25 MB default archive limit) that cancel downstream connections immediately upon breach.
32
-
- Streaming Decompression Defense via `fflate`: Archive buffers are fed in bounded 64 KiB chunks with a hard all-entry cap (20,000 max entries) against header bombs, candidate file limits (10,000 max), path traversal rejection, null-byte path validation, path depth limits (32 max), per-entry uncompressed byte caps (1 MB max, skipping oversized entries early), declared compression-ratio heuristic guards (aborting on >200:1 ratio past 256 KB floor), and cumulative extracted content limits (100 MB max, counting all emitted bytes).
32
+
- Streaming Decompression Defense via `fflate`: Archive buffers are fed in bounded 64 KiB chunks with a hard all-entry cap (20,000 max entries) against header bombs, candidate file limits (10,000 max), path traversal rejection, null-byte path validation, path depth limits (32 max), per-entry uncompressed byte caps (1 MB max, skipping oversized entries early), declared compression-ratio quarantine (>200:1 ratio past a 256 KB floor terminates and discards that entry, skipped with a `suspicious_compression_ratio` diagnostic, while analysis of the remaining safe files continues; every emitted byte—including quarantined entries—still counts toward the cumulative caps), and cumulative extracted content limits (100 MB max, counting all emitted bytes). Public durable analyses raise these bounds (20,000 files / 100,000 entries / 128 MB archive / 192 MB extracted) with honest partial inventories instead of hard failures.
|**Malicious Archive / ZIP Bomb**| Attacker crafts a ZIP with high compression ratio (e.g. 5 MB expanding to 10 GB), millions of files, or duplicate paths. |**Critical**| Streaming decompression via `fflate` with bounded ingestion: 64 KiB chunk-fed `Unzip` (`UnzipInflate`/`UnzipPassThrough`), EOCD structural validation, `maxArchiveEntries = 20,000` header-bomb cap (`TOO_MANY_ARCHIVE_ENTRIES`), `maxFiles = 10,000` candidate cap (`TOO_MANY_FILES`), duplicate normalized-path rejection, per-entry live cap `maxFileBytes = 1 MB` with early `terminate()` and discarded buffers, cumulative `maxTotalExtractedBytes = 100 MB` counting every emitted byte including skipped oversized files (`EXTRACTED_TOO_LARGE`), declared ratio heuristic >200:1 after 256 KB floor (`SUSPICIOUS_COMPRESSION_RATIO`), and `maxArchiveBytes = 25 MB` network cap with streaming `reader.cancel()`. Local folder byte limits use `File.size` before decoding so UTF-16 string length cannot under-count input. |
11
+
| **Malicious Archive / ZIP Bomb** | Attacker crafts a ZIP with high compression ratio (e.g. 5 MB expanding to 10 GB), millions of files, or duplicate paths. | **Critical** | Streaming decompression via `fflate` with bounded ingestion: 64 KiB chunk-fed `Unzip` (`UnzipInflate`/`UnzipPassThrough`), EOCD structural validation, `maxArchiveEntries = 20,000` header-bomb cap (`TOO_MANY_ARCHIVE_ENTRIES`), `maxFiles = 10,000` candidate cap (`TOO_MANY_FILES`), duplicate normalized-path rejection, per-entry live cap `maxFileBytes = 1 MB` with early `terminate()` and discarded buffers, cumulative `maxTotalExtractedBytes = 100 MB` counting every emitted byte including skipped oversized files (`EXTRACTED_TOO_LARGE`), declared ratio quarantine >200:1 after 256 KB floor (entry stream `terminate()`d and buffers discarded, skipped with a `suspicious_compression_ratio` diagnostic while analysis of safe files continues—no `413` is raised for a quarantined entry), and `maxArchiveBytes = 25 MB` network cap with streaming `reader.cancel()`. These are the browser/private defaults; public durable analyses raise them to 20,000 files / 100,000 entries / 128 MB archive / 192 MB extracted with honest partial inventories (`allowPartialOnFileLimit`). Local folder byte limits use `File.size` before decoding so UTF-16 string length cannot under-count input. |
12
12
|**Path Traversal / Arbitrary File Overwrite**| Archive entries contain `../../etc/passwd`, absolute paths (`/root`), or Windows drive letters (`C:\`). |**Critical**|`validatePath`/`normalizeArchivePath` reject absolute paths, drive letters, `..` segments and null bytes before extraction; path depth is capped at 32 segments and paths longer than 4,096 characters are skipped. `PATH_TRAVERSAL` fails closed at the incremental `onfile` boundary, not post-extraction. |
13
13
|**Malformed or hostile analysis artifact**| A user imports a huge or schema-invalid RepoDNA JSON file, or a cached Blob is corrupted. |**High**| Imported artifacts are capped at 128 MB before reading and validated through the version-aware schema loader. The CSP-safe browser validator checks every graph node and edge with a bounded error list. Cached public artifacts are size checked and schema validated before use; malformed entries are evicted. |
14
14
|**Malicious Syntax / Parser Bomb**| Deeply nested ASTs, 1 MB single-line JS strings, or recursive macros designed to crash memory/CPU. |**High**| Tree-sitter budgets `MAX_AST_DEPTH = 128`, `MAX_AST_NODES = 25,000` with item collection limits and guaranteed `tree.delete()` cleanup. Syntax parsing runs in try-catch with graceful degradation (`parseSuccessRate` reduction and diagnostics) without failing the whole repo. Future: wall-clock analysis budget / Worker isolation. |
0 commit comments