Przeglądaj źródła

fix(mcp): make session-marker symlink resistance work on Windows (#337)

O_NOFOLLOW is undefined on Windows (libuv ignores it), so the bitwise-OR
silently dropped it and markSessionConsulted would follow a pre-planted symlink
at the tmp marker path — the CWE-59 gap #280 closed on POSIX but not Windows.
Add a cross-platform lstatSync isSymbolicLink() refuse-check before openSync
(O_NOFOLLOW stays as the atomic, TOCTOU-free guard on POSIX). The existing
Session-marker-symlink-resistance test now passes on Windows.

Refs #280

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Colby Mchenry 1 miesiąc temu
rodzic
commit
6f4b521512
1 zmienionych plików z 11 dodań i 0 usunięć
  1. 11 0
      src/mcp/tools.ts

+ 11 - 0
src/mcp/tools.ts

@@ -11,6 +11,7 @@ import {
   constants as fsConstants,
   closeSync,
   existsSync,
+  lstatSync,
   openSync,
   readFileSync,
   writeSync,
@@ -224,6 +225,16 @@ function markSessionConsulted(sessionId: string): void {
   try {
     const hash = createHash('md5').update(sessionId).digest('hex').slice(0, 16);
     const markerPath = join(tmpdir(), `codegraph-consulted-${hash}`);
+    // Refuse to follow a pre-planted symlink at the marker path (CWE-59).
+    // O_NOFOLLOW (below) is the atomic, TOCTOU-free guard on POSIX, but it is
+    // `undefined` on Windows (libuv ignores it), so the bitwise-OR silently
+    // drops it and openSync would follow the link. This lstat check closes that
+    // gap cross-platform; ENOENT (path is free) falls through to create it.
+    try {
+      if (lstatSync(markerPath).isSymbolicLink()) return;
+    } catch {
+      // No existing entry (or stat failed) — nothing to refuse; proceed.
+    }
     // O_NOFOLLOW makes openSync throw ELOOP if markerPath is already a symlink.
     // O_CREAT + O_TRUNC keep the original "create-or-overwrite" semantics, and
     // mode 0o600 prevents readback by other local users (the marker payload is