Skip to content

Pre release security auditing#27

Merged
vagkaratzas merged 11 commits into
v3from
pre-release-security-auditing
Jul 8, 2026
Merged

Pre release security auditing#27
vagkaratzas merged 11 commits into
v3from
pre-release-security-auditing

Conversation

@vagkaratzas

Copy link
Copy Markdown
Collaborator

High-priority fixes:

clustering.py: added MAX_ALL_PAIRS_MEMBERS = 1000. Communities at/below the cap keep the exact v2 all-pairs padding; above it, members connect to a single O(n) virtual hub (dropped from the returned coords) — same "keep disconnected members spread" effect without the quadratic edge count. Verified: an 8 000-member community now peaks at +3.2 MB / 0.064 s (was multiple GB). New regression test test_large_community_uses_hub_not_all_pairs; full suite 66 passed.

external.py: TTL cut from 24 h → 1 h; added MAX_TOKENS = 1000 enforced by _enforce_cap(), which runs before every write and evicts the oldest sessions (not reject — so an attacker can't deny the feature by keeping the store full). Combined with the per-request MAX_UPLOAD_BYTES cap, tmp/ is now hard-bounded to MAX_TOKENS * MAX_UPLOAD_BYTES regardless of request volume. New regression test test_external_storage_capped (posts 20 with cap 5, asserts ≤ 5 files remain); full suite 67 passed. Single-use delete-on-resolve was not taken — it would 404 a legitimate page refresh; TTL + count cap bound disk without that UX cost. Proxy limit_req and the chunked-transfer note remain deployment-side recommendations.

Medium-priority fixes:

┌─────────┬────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ Commit  │          Finding           │                                       Fix                                        │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 15ac22b │ #1 malformed TSV → 500     │ catch EmptyDataError + reject empty mandatory cells → 400                        │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ d0d5620 │ #2 non-dict session        │ guard layers/nodes/edges are lists of objects → 400                              │
│         │ members → 500              │                                                                                  │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 4068d63 │ #3+#4 weights              │ clamp non-positive/non-finite scaled_weight in shared build_graph + parser       │
│         │                            │ rejects non-finite TSV weights                                                   │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 675d2bb │ #5 escapeHtml              │ add ' → '                                                                    │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 29a6092 │ #6 channel color           │ escape color in value="…"                                                        │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 33793e5 │ #7 external token          │ encodeURIComponent(token)                                                        │
├─────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ 04c5172 │ #8 rate limiting           │ nginx lapi/                                                                      │
└─────────┴────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

vagkaratzas and others added 11 commits July 8, 2026 20:13
…_PAIRS_MEMBERS and one virtual hub fallback option
/api/external wrote a session file per request with no cap on count or
total bytes, reclaimed only by a 24h age sweep. An unauthenticated caller
could fill disk with ~20MB session files faster than the sweep reclaims.

- TTL 24h -> 1h (handoff tokens are consumed within minutes)
- add MAX_TOKENS=1000, enforced before every write via _enforce_cap(),
  evicting oldest sessions (evict, not reject, so the feature can't be
  denied to legit callers by keeping the store full)
- tmp/ now hard-bounded to MAX_TOKENS * MAX_UPLOAD_BYTES

Regression test: post 20 with cap 5, assert <=5 files remain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parse_network_tsv leaked two uncaught exceptions as HTTP 500s:
- empty upload -> pandas EmptyDataError
- blank mandatory cell -> NaN -> EdgeModel ValidationError

Catch EmptyDataError and reject empty/whitespace mandatory cells in
_validate, both as NetworkValidationError -> clean 400.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_validate assumed every layers/nodes/edges entry is a dict and called
.get()/subscripts on it, so a payload like {"layers":["x"],...} 500'd with
AttributeError. Reachable from /api/session/import and /api/external.
Guard that each is a list of objects, raising SessionValidationError.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- topology betweenness 500'd on non-positive scaled_weight, and inf/nan
  weights propagated into responses as invalid JSON.
- build_graph now clamps any scaled_weight that isn't strictly-positive-
  finite to a small floor, fixing both at the single point layout and
  topology share. Layout ignores weights, so only topology input changes.
- parser rejects non-finite Weight values at parse time, keeping the
  /api/network response finite.

Clamped (not rejected at the model): a Field(allow_inf_nan=False) makes
FastAPI's 422 handler try to echo the inf input and itself 500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Escaped & < > " but not ' — a latent XSS footgun for any single-quoted
attribute template. Add the ' -> &#39; replacement so the helper is safe
by default regardless of quote style at the call site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An imported session's edge color flows into channelColors unvalidated and
was interpolated raw into a type="color" value="..." attribute inside an
innerHTML template. CSP blocks execution, but escape it so the attribute
can't be broken out of. Defense-in-depth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolveExternal interpolated the ?session= token straight into the fetch
path; a '/' or '?' in it builds a malformed URL. Wrap with
encodeURIComponent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
/api/layout and /api/topology run O(V*E) betweenness with no auth; per-
request size caps bound one request but not the rate. Add an nginx
limit_req (20 r/s, burst 40 nodelay, per client IP) — interactive bursts
pass, sustained floods throttle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vagkaratzas
vagkaratzas merged commit 27887da into v3 Jul 8, 2026
3 checks passed
@vagkaratzas
vagkaratzas deleted the pre-release-security-auditing branch July 8, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant