cf-memory-mcp 3.9.6 → 3.9.7
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.
- package/bin/cf-memory-mcp.js +71 -5
- package/package.json +1 -1
package/bin/cf-memory-mcp.js
CHANGED
|
@@ -867,11 +867,29 @@ class CFMemoryMCP {
|
|
|
867
867
|
|
|
868
868
|
// Read each file locally. Note: uploadFileBatch expects `relativePath`
|
|
869
869
|
// (not `path`) — it maps to `file_path` server-side.
|
|
870
|
+
//
|
|
871
|
+
// Confine each file to project_root with the same realpath-aware
|
|
872
|
+
// check used in maybeServeLocalFileContent. Without this, an
|
|
873
|
+
// attacker could pass file_paths: ["/etc/hosts"] and pollute the
|
|
874
|
+
// indexed corpus with arbitrary local files. Same vulnerability
|
|
875
|
+
// class as the get_file_content fix (codex review).
|
|
876
|
+
const normalizedRoot = (() => {
|
|
877
|
+
try { return fs.realpathSync(projectRoot); }
|
|
878
|
+
catch (_) { return projectRoot; }
|
|
879
|
+
})();
|
|
870
880
|
const files = [];
|
|
871
881
|
const skipped = [];
|
|
872
882
|
for (const rel of filePaths) {
|
|
873
883
|
try {
|
|
874
|
-
const
|
|
884
|
+
const candidate = path.resolve(normalizedRoot, rel);
|
|
885
|
+
let full;
|
|
886
|
+
try { full = fs.realpathSync(candidate); }
|
|
887
|
+
catch (_) { skipped.push({ path: rel, reason: 'not readable' }); continue; }
|
|
888
|
+
const relInside = path.relative(normalizedRoot, full);
|
|
889
|
+
if (!relInside || relInside.startsWith('..') || path.isAbsolute(relInside)) {
|
|
890
|
+
skipped.push({ path: rel, reason: 'path escapes project_root' });
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
875
893
|
const stat = fs.statSync(full);
|
|
876
894
|
if (!stat.isFile()) {
|
|
877
895
|
skipped.push({ path: rel, reason: 'not a file' });
|
|
@@ -964,6 +982,15 @@ class CFMemoryMCP {
|
|
|
964
982
|
// Each file in the list already carries file_hash + indexed_at
|
|
965
983
|
// (list_files was extended to return them), so we don't need any
|
|
966
984
|
// additional server roundtrips. Just hash the local files.
|
|
985
|
+
//
|
|
986
|
+
// Defense in depth: indexed file_paths SHOULD be relative-to-root,
|
|
987
|
+
// but if a previous-version bridge indexed an absolute path, this
|
|
988
|
+
// loop would have read /etc/hosts to compare hashes. Confine to the
|
|
989
|
+
// project root via realpath check just like get_file_content.
|
|
990
|
+
const normalizedRoot = (() => {
|
|
991
|
+
try { return fs.realpathSync(projectRoot); }
|
|
992
|
+
catch (_) { return projectRoot; }
|
|
993
|
+
})();
|
|
967
994
|
const stale = [];
|
|
968
995
|
const missing = [];
|
|
969
996
|
const fresh = [];
|
|
@@ -973,7 +1000,18 @@ class CFMemoryMCP {
|
|
|
973
1000
|
const indexedAt = f.indexed_at;
|
|
974
1001
|
if (!indexedHash) continue;
|
|
975
1002
|
|
|
976
|
-
const
|
|
1003
|
+
const candidate = path.resolve(normalizedRoot, filePath);
|
|
1004
|
+
let full;
|
|
1005
|
+
try { full = fs.realpathSync(candidate); }
|
|
1006
|
+
catch (_) {
|
|
1007
|
+
missing.push({ file_path: filePath, indexed_at: indexedAt, reason: 'file missing locally' });
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
const relInside = path.relative(normalizedRoot, full);
|
|
1011
|
+
if (!relInside || relInside.startsWith('..') || path.isAbsolute(relInside)) {
|
|
1012
|
+
missing.push({ file_path: filePath, indexed_at: indexedAt, reason: 'indexed path escapes project_root (skipped for safety)' });
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
977
1015
|
let content;
|
|
978
1016
|
try {
|
|
979
1017
|
content = fs.readFileSync(full, 'utf8');
|
|
@@ -1049,13 +1087,27 @@ class CFMemoryMCP {
|
|
|
1049
1087
|
return respond({ error: 'Failed to list indexed files' });
|
|
1050
1088
|
}
|
|
1051
1089
|
|
|
1090
|
+
// Same defense as handleFindStaleFiles: confine indexed file_paths
|
|
1091
|
+
// to the project root via realpath check.
|
|
1092
|
+
const normalizedRoot = (() => {
|
|
1093
|
+
try { return fs.realpathSync(projectRoot); }
|
|
1094
|
+
catch (_) { return projectRoot; }
|
|
1095
|
+
})();
|
|
1052
1096
|
const staleFiles = [];
|
|
1053
1097
|
const missing = [];
|
|
1054
1098
|
for (const f of filesList) {
|
|
1055
1099
|
const filePath = f.file_path;
|
|
1056
1100
|
const indexedHash = f.file_hash;
|
|
1057
1101
|
if (!indexedHash) continue;
|
|
1058
|
-
const
|
|
1102
|
+
const candidate = path.resolve(normalizedRoot, filePath);
|
|
1103
|
+
let full;
|
|
1104
|
+
try { full = fs.realpathSync(candidate); }
|
|
1105
|
+
catch (_) { missing.push(filePath); continue; }
|
|
1106
|
+
const relInside = path.relative(normalizedRoot, full);
|
|
1107
|
+
if (!relInside || relInside.startsWith('..') || path.isAbsolute(relInside)) {
|
|
1108
|
+
missing.push(filePath); // indexed path escapes root
|
|
1109
|
+
continue;
|
|
1110
|
+
}
|
|
1059
1111
|
let content;
|
|
1060
1112
|
try {
|
|
1061
1113
|
content = fs.readFileSync(full, 'utf8');
|
|
@@ -1912,11 +1964,25 @@ class CFMemoryMCP {
|
|
|
1912
1964
|
|
|
1913
1965
|
// Try to find a local project root. We watch CF_MEMORY_WATCH_PATH
|
|
1914
1966
|
// (when auto-watch is on) or fall back to the cwd.
|
|
1915
|
-
const
|
|
1967
|
+
const rawRoot = process.env.CF_MEMORY_WATCH_PATH || process.cwd();
|
|
1968
|
+
// Realpath + relative-path check — same defense as
|
|
1969
|
+
// maybeServeLocalFileContent. Server-supplied file_paths SHOULD be
|
|
1970
|
+
// relative-to-root, but if an old or buggy index stored an
|
|
1971
|
+
// absolute or escaping path, we'd otherwise read it (and reveal
|
|
1972
|
+
// its hash via the stale flag).
|
|
1973
|
+
const root = (() => {
|
|
1974
|
+
try { return fs.realpathSync(rawRoot); }
|
|
1975
|
+
catch (_) { return rawRoot; }
|
|
1976
|
+
})();
|
|
1916
1977
|
const stalePaths = new Set();
|
|
1917
1978
|
for (const r of results) {
|
|
1918
1979
|
if (!r || !r.file_path || !r.indexed_file_hash) continue;
|
|
1919
|
-
const
|
|
1980
|
+
const candidate = path.resolve(root, r.file_path);
|
|
1981
|
+
let full;
|
|
1982
|
+
try { full = fs.realpathSync(candidate); }
|
|
1983
|
+
catch (_) { continue; }
|
|
1984
|
+
const relInside = path.relative(root, full);
|
|
1985
|
+
if (!relInside || relInside.startsWith('..') || path.isAbsolute(relInside)) continue;
|
|
1920
1986
|
let content;
|
|
1921
1987
|
try { content = fs.readFileSync(full, 'utf8'); } catch (_) { continue; }
|
|
1922
1988
|
// SHA-256 hex of UTF-8 content. Matches the server's hashContent().
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cf-memory-mcp",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.7",
|
|
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": {
|