Skip to content

fix: connector path/id/webrtc hardening (#538, #539, part of #536) - #18

Merged
lukepolo merged 2 commits into
mainfrom
audit/connector-hardening
Jul 12, 2026
Merged

fix: connector path/id/webrtc hardening (#538, #539, part of #536)#18
lukepolo merged 2 commits into
mainfrom
audit/connector-hardening

Conversation

@Flegma

@Flegma Flegma commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Hardens three findings from the 2026-07 audit that are self-contained and non-breaking. Closes #538, #539; partially addresses #536 (see note).

Changes

Path traversal on offline-match ids (#538)

  • MatchData.id is now validated with @Matches(/^[A-Za-z0-9_-]+$/). It is interpolated into /pod-manifests/<id>.yaml and .json, so a ..// value could escape that directory. Match ids are UUIDs, which the pattern accepts.
  • The derived manifest paths in offline-matches.service.ts now go through a manifestPath() helper that applies path.basename, as defense-in-depth.

File-operations path boundary (part of #536)

  • validatePath used startsWith, so a sibling directory sharing a prefix (for example /custom-plugins-evil) was accepted as an allowed base. Replaced with an isWithin boundary check (child === root || child.startsWith(root + sep)) for both the allowlist and the traversal containment check. Legitimate /servers/<id> and /custom-plugins[/...] paths are unaffected.

WebRTC connection leak (#539)

  • WebrtcService never closed PeerConnections or removed them from pcMap, so every region latency test leaked a native connection (and its closures) for the pod's lifetime. Now: connections are closed and removed on terminal ICE state (disconnected/failed/closed), a re-offer for the same peerId closes the previous connection first, and onModuleDestroy closes any remaining. node-datachannel requires an explicit close() to free native resources.

Not included here (needs a decision, deliberately deferred)

The authentication halves of #536 and #537 are not in this PR. The api's FileManagerService calls /file-operations/* directly over http://<nodeIP>:8585 with no Authorization header (it relies on api-side admin checks), and the RCON WS gateway has the same caller coupling. Adding BasicGuardGuard (the issue's suggested fix) would break the file manager and RCON unless the api is taught to present a shared credential, or the connector's HTTP/WS port is restricted by a NetworkPolicy. That is an architectural choice that overlaps with #413, so it is left for a coordinated change. Details noted on #536/#537.

Testing

  • yarn build: clean.
  • New specs: file-operations.service.spec.ts (4, path boundary incl. the sibling-prefix regression) and MatchData.spec.ts (3, id validation) pass. Note: the connector's existing jest suite is broken on main (no src/ path-alias moduleNameMapper, and class-transformer needs reflect-metadata); the new specs use relative imports and import reflect-metadata, so they run. The broken suite config is a separate pre-existing issue worth its own ticket.

Flegma added 2 commits July 10, 2026 21:45
…leak

- validatePath: enforce allowlist/containment on a path boundary instead of
  startsWith, so a sibling dir sharing a prefix (e.g. /custom-plugins-evil)
  is no longer accepted (part of #536).
- MatchData.id: restrict to path-safe characters so it cannot escape
  /pod-manifests when interpolated into file paths; route the derived
  manifest paths through basename as defense-in-depth (#538).
- WebrtcService: close native PeerConnections and drop them from pcMap on
  terminal ICE state, dedupe on re-offer, and close all on shutdown, fixing
  an unbounded native-resource/map leak per region latency test (#539).

Adds focused specs for the path boundary and id validation.
…eview follow-up)

Review caught a race: node-datachannel delivers a connection's 'closed' state
change on a later tick, so a re-offer that replaced the map entry would have
its stale event close the NEW connection (key-only lookup). closePeerConnection
now takes the specific instance, only evicts the map entry when it still points
at that instance, and a WeakSet prevents double-close. onStateChange closes its
own connection; dedupe/shutdown target the current entry.
@Flegma

Flegma commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Independent code review run on the diff. It caught a real race in the WebRTC cleanup: node-datachannel delivers a connection's closed state change on a later tick, so a re-offer that replaced the map entry would have its stale event close the new connection (the cleanup looked up by key, not instance). Fixed in d0a346b: close by instance identity, evict the map entry only when it still points at that instance, and a WeakSet prevents double-close. Build + specs green.

@lukepolo
lukepolo merged commit 85b006d into main Jul 12, 2026
1 check passed
@lukepolo
lukepolo deleted the audit/connector-hardening branch July 12, 2026 11:03
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.

2 participants