cf-memory-mcp 3.9.5 → 3.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1808,11 +1808,27 @@ class CFMemoryMCP {
1808
1808
  // files because path.resolve treats absolute file_paths as-is and
1809
1809
  // doesn't enforce ancestry. This is a security finding from codex
1810
1810
  // review; fix before reading anything off disk.
1811
- const normalizedRoot = path.resolve(projectRoot);
1812
- const resolvedFull = path.resolve(normalizedRoot, filePath);
1813
- // Path must be strictly under the project root. `relative` returns
1814
- // an empty string for the root itself and a `..`-prefixed string
1815
- // for anything outside.
1811
+ //
1812
+ // Also dereference symlinks via fs.realpathSync — without this,
1813
+ // an attacker could create a symlink inside the project root
1814
+ // pointing at /etc/passwd and the relative-path check would
1815
+ // still pass (the symlink itself is in-root). realpath gives
1816
+ // the underlying target which we then check the same way.
1817
+ const normalizedRoot = (() => {
1818
+ try { return fs.realpathSync(path.resolve(projectRoot)); }
1819
+ catch (_) { return path.resolve(projectRoot); }
1820
+ })();
1821
+ const candidateFull = path.resolve(normalizedRoot, filePath);
1822
+ let resolvedFull;
1823
+ try {
1824
+ resolvedFull = fs.realpathSync(candidateFull);
1825
+ } catch (_) {
1826
+ return false; // doesn't exist or unreadable
1827
+ }
1828
+ // Path must be strictly under the (real) project root. `relative`
1829
+ // returns an empty string for the root itself and a `..`-prefixed
1830
+ // string for anything outside. Use the realpath of both sides so
1831
+ // symlinks can't smuggle the target out of the root.
1816
1832
  const relative = path.relative(normalizedRoot, resolvedFull);
1817
1833
  if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
1818
1834
  _mcpTrace('GFC_LOCAL_REJECT_ESCAPE', `${filePath} -> ${resolvedFull} escapes root ${normalizedRoot}`);
@@ -1856,7 +1872,11 @@ class CFMemoryMCP {
1856
1872
  const language = extToLang[ext] || null;
1857
1873
 
1858
1874
  const payload = {
1859
- file_path: path.relative(projectRoot, resolvedFull) || path.basename(resolvedFull),
1875
+ // Use normalizedRoot (the realpath'd root) to match resolvedFull
1876
+ // (also realpath'd). Using the original projectRoot here gave
1877
+ // weird relative paths on macOS where /tmp is a symlink to
1878
+ // /private/tmp.
1879
+ file_path: path.relative(normalizedRoot, resolvedFull) || path.basename(resolvedFull),
1860
1880
  language,
1861
1881
  total_lines: totalLines,
1862
1882
  size_bytes: stat.size,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cf-memory-mcp",
3
- "version": "3.9.5",
3
+ "version": "3.9.6",
4
4
  "description": "Cloudflare-hosted MCP server for code indexing, retrieval, and assistant memory with a direct remote MCP endpoint and local stdio bridge.",
5
5
  "main": "bin/cf-memory-mcp.js",
6
6
  "bin": {