magector 2.15.0 → 2.15.1
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/README.md +31 -0
- package/package.json +5 -5
- package/src/mcp-server.js +67 -7
- package/src/update.js +18 -3
package/README.md
CHANGED
|
@@ -136,6 +136,37 @@ flowchart LR
|
|
|
136
136
|
|
|
137
137
|
---
|
|
138
138
|
|
|
139
|
+
## Security
|
|
140
|
+
|
|
141
|
+
Magector operates on source code indexed from potentially-untrusted `vendor/` dependencies and is driven by an LLM that may be manipulated via prompt injection in indexed comments, docblocks, or markdown. The following hardening applies as of **v2.15.1**:
|
|
142
|
+
|
|
143
|
+
### Path traversal protection
|
|
144
|
+
|
|
145
|
+
All tools that accept a `path` argument (`magento_read`, `magento_grep`, `magento_ast_search`, `magento_find_dataobject_issues`) route the input through `safePath()` / `safeRelPath()` helpers in `src/mcp-server.js`. These:
|
|
146
|
+
|
|
147
|
+
1. Resolve the argument against `MAGENTO_ROOT` with `path.resolve()` (normalizes `..`, symlinks are not followed during validation).
|
|
148
|
+
2. Reject any resolved path that does not lie inside `MAGENTO_ROOT`.
|
|
149
|
+
|
|
150
|
+
This prevents a hostile `vendor/` comment from instructing the LLM to e.g. `magento_read` `../../home/user/.ssh/id_rsa`. Both the standalone case handlers and their `magento_batch` counterparts share the same chokepoint.
|
|
151
|
+
|
|
152
|
+
### Shell injection hardening in auto-update
|
|
153
|
+
|
|
154
|
+
`src/update.js` fetches the `latest` field from the npm registry and re-execs itself with the new version string. Previously this was interpolated into a shell command; a tampered registry response could inject shell metacharacters. As of v2.15.1:
|
|
155
|
+
|
|
156
|
+
- The re-exec passes argv as an **array** to a no-shell spawner (no intermediate shell).
|
|
157
|
+
- A semver-strict `isSafeVersion()` validator rejects any version string containing metacharacters or that does not match `X.Y.Z` / `X.Y.Z-prerelease` form.
|
|
158
|
+
- Fails closed: the auto-update is silently skipped rather than run a malformed version.
|
|
159
|
+
|
|
160
|
+
### Unix socket permissions
|
|
161
|
+
|
|
162
|
+
The serve-proxy Unix socket at `.magector/serve.sock` is created with `chmod 0600` immediately after `listen()`. On multi-user systems, another local account can no longer connect and query the vector index (which would leak indexed source snippets). The chmod is best-effort on platforms that don't support it (logged to `.magector/magector.log`).
|
|
163
|
+
|
|
164
|
+
### Reporting vulnerabilities
|
|
165
|
+
|
|
166
|
+
If you find a security issue, please open an issue on the GitHub repo and mark it as security-related. Do not post reproducers that leak actual source contents from private codebases.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
139
170
|
## Quick Start
|
|
140
171
|
|
|
141
172
|
### Prerequisites
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.1",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"ruvector": "^0.1.96"
|
|
34
34
|
},
|
|
35
35
|
"optionalDependencies": {
|
|
36
|
-
"@magector/cli-darwin-arm64": "2.15.
|
|
37
|
-
"@magector/cli-linux-x64": "2.15.
|
|
38
|
-
"@magector/cli-linux-arm64": "2.15.
|
|
39
|
-
"@magector/cli-win32-x64": "2.15.
|
|
36
|
+
"@magector/cli-darwin-arm64": "2.15.1",
|
|
37
|
+
"@magector/cli-linux-x64": "2.15.1",
|
|
38
|
+
"@magector/cli-linux-arm64": "2.15.1",
|
|
39
|
+
"@magector/cli-win32-x64": "2.15.1"
|
|
40
40
|
},
|
|
41
41
|
"keywords": [
|
|
42
42
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { execFileSync, spawn } from 'child_process';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
19
|
import { createServer as createNetServer, createConnection } from 'net';
|
|
20
|
-
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, constants as fsConstants } from 'fs';
|
|
20
|
+
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, chmodSync, constants as fsConstants } from 'fs';
|
|
21
21
|
import { stat } from 'fs/promises';
|
|
22
22
|
import { glob } from 'glob';
|
|
23
23
|
import path from 'path';
|
|
@@ -149,6 +149,38 @@ const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
|
|
|
149
149
|
const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
|
|
150
150
|
const PRIMARY_LOCK_PATH = path.join(config.magentoRoot, '.magector', 'primary.lock');
|
|
151
151
|
|
|
152
|
+
// ─── Path Safety ────────────────────────────────────────────────
|
|
153
|
+
// All tool handlers accept user-supplied paths relative to MAGENTO_ROOT.
|
|
154
|
+
// Without validation, `../../../etc/passwd` would escape the project.
|
|
155
|
+
// A hostile indexed file could prompt-inject the LLM into requesting such a
|
|
156
|
+
// path and leak host files. These helpers are the single chokepoint.
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve a user-supplied path against a trusted root and verify it stays
|
|
160
|
+
* inside the root. Returns the absolute resolved path or null on escape.
|
|
161
|
+
*/
|
|
162
|
+
function safePath(root, rel) {
|
|
163
|
+
if (rel === undefined || rel === null) return null;
|
|
164
|
+
const rootAbs = path.resolve(root);
|
|
165
|
+
const joined = path.resolve(rootAbs, String(rel));
|
|
166
|
+
if (joined !== rootAbs && !joined.startsWith(rootAbs + path.sep)) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
return joined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Like safePath but returns the relative form (used for tools that invoke
|
|
174
|
+
* external processes with cwd=root and want a relative path argument).
|
|
175
|
+
* '.' means "the root itself".
|
|
176
|
+
*/
|
|
177
|
+
function safeRelPath(root, rel) {
|
|
178
|
+
const abs = safePath(root, rel);
|
|
179
|
+
if (!abs) return null;
|
|
180
|
+
const r = path.relative(path.resolve(root), abs);
|
|
181
|
+
return r === '' ? '.' : r;
|
|
182
|
+
}
|
|
183
|
+
|
|
152
184
|
/**
|
|
153
185
|
* Expand brace patterns in include globs for GNU grep compatibility.
|
|
154
186
|
* GNU grep --include does NOT support brace expansion (that's a shell feature).
|
|
@@ -737,7 +769,13 @@ function startSocketProxy() {
|
|
|
737
769
|
logToFile('WARN', `Socket proxy error: ${err.message}`);
|
|
738
770
|
});
|
|
739
771
|
socketServer.listen(SOCK_PATH, () => {
|
|
740
|
-
|
|
772
|
+
// Restrict socket to the owning user — without this, on multi-user
|
|
773
|
+
// systems any local account could connect to the serve proxy and query
|
|
774
|
+
// the index (leaking indexed code snippets to other local users).
|
|
775
|
+
try { chmodSync(SOCK_PATH, 0o600); } catch (err) {
|
|
776
|
+
logToFile('WARN', `Failed to chmod socket to 0600: ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
logToFile('INFO', `Socket proxy listening on ${SOCK_PATH} (mode 0600)`);
|
|
741
779
|
});
|
|
742
780
|
}
|
|
743
781
|
|
|
@@ -3663,7 +3701,11 @@ async function astSearch(pattern, searchPath, lang, maxResults) {
|
|
|
3663
3701
|
const root = config.magentoRoot;
|
|
3664
3702
|
if (!root) throw new Error('MAGENTO_ROOT not set');
|
|
3665
3703
|
|
|
3666
|
-
const targetPath = searchPath ?
|
|
3704
|
+
const targetPath = searchPath ? safePath(root, searchPath) : path.resolve(root);
|
|
3705
|
+
if (!targetPath) {
|
|
3706
|
+
logToFile('WARN', `ast_search: rejected path traversal attempt: "${searchPath}"`);
|
|
3707
|
+
throw new Error(`Path escapes project root: ${searchPath}`);
|
|
3708
|
+
}
|
|
3667
3709
|
const semgrepLang = lang || 'php';
|
|
3668
3710
|
const limit = Math.min(maxResults || 50, 200);
|
|
3669
3711
|
|
|
@@ -6466,7 +6508,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6466
6508
|
break;
|
|
6467
6509
|
}
|
|
6468
6510
|
case 'magento_read': {
|
|
6469
|
-
const filePath =
|
|
6511
|
+
const filePath = safePath(config.magentoRoot, a.path);
|
|
6512
|
+
if (!filePath) {
|
|
6513
|
+
logToFile('WARN', `batch read: rejected path traversal attempt: "${a.path}"`);
|
|
6514
|
+
text = `Path escapes project root: ${a.path}`;
|
|
6515
|
+
break;
|
|
6516
|
+
}
|
|
6470
6517
|
let fileContent;
|
|
6471
6518
|
try { fileContent = readFileSync(filePath, 'utf-8'); } catch {
|
|
6472
6519
|
text = `File not found: ${a.path}`;
|
|
@@ -6491,7 +6538,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6491
6538
|
break;
|
|
6492
6539
|
}
|
|
6493
6540
|
case 'magento_grep': {
|
|
6494
|
-
const searchPath = a.path || '.';
|
|
6541
|
+
const searchPath = safeRelPath(config.magentoRoot, a.path || '.');
|
|
6542
|
+
if (!searchPath) {
|
|
6543
|
+
logToFile('WARN', `batch grep: rejected path traversal attempt: "${a.path}"`);
|
|
6544
|
+
text = `Path escapes project root: ${a.path}`;
|
|
6545
|
+
break;
|
|
6546
|
+
}
|
|
6495
6547
|
const include = a.include || '*.php';
|
|
6496
6548
|
const maxRes = Math.min(a.maxResults || 30, 100);
|
|
6497
6549
|
const batchCtx = a.context !== undefined ? a.context : 4;
|
|
@@ -6569,7 +6621,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6569
6621
|
case 'magento_grep': {
|
|
6570
6622
|
const root = config.magentoRoot;
|
|
6571
6623
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6572
|
-
const searchPath = args.path || '.';
|
|
6624
|
+
const searchPath = safeRelPath(root, args.path || '.');
|
|
6625
|
+
if (!searchPath) {
|
|
6626
|
+
logToFile('WARN', `grep: rejected path traversal attempt: "${args.path}"`);
|
|
6627
|
+
return { content: [{ type: 'text', text: `Path escapes project root: ${args.path}` }], isError: true };
|
|
6628
|
+
}
|
|
6573
6629
|
const include = args.include || '*.php';
|
|
6574
6630
|
const maxResults = Math.min(args.maxResults || 50, 200);
|
|
6575
6631
|
const ctxLines = args.context !== undefined ? args.context : 4;
|
|
@@ -6756,7 +6812,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6756
6812
|
case 'magento_read': {
|
|
6757
6813
|
const root = config.magentoRoot;
|
|
6758
6814
|
if (!root) return { content: [{ type: 'text', text: 'MAGENTO_ROOT not set.' }], isError: true };
|
|
6759
|
-
const filePath =
|
|
6815
|
+
const filePath = safePath(root, args.path);
|
|
6816
|
+
if (!filePath) {
|
|
6817
|
+
logToFile('WARN', `read: rejected path traversal attempt: "${args.path}"`);
|
|
6818
|
+
return { content: [{ type: 'text', text: `Path escapes project root: ${args.path}` }], isError: true };
|
|
6819
|
+
}
|
|
6760
6820
|
let content;
|
|
6761
6821
|
try { content = readFileSync(filePath, 'utf-8'); } catch (err) {
|
|
6762
6822
|
logToFile('WARN', `read: file not found: ${args.path} (${err.code || err.message})`);
|
package/src/update.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Never blocks the CLI on failure — network errors are silently ignored.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
|
-
import {
|
|
11
|
+
import { execFileSync } from 'child_process';
|
|
12
12
|
import { homedir } from 'os';
|
|
13
13
|
import path from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
@@ -140,14 +140,29 @@ export async function checkForUpdate(command, originalArgs) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Validate a semver string to prevent shell injection via malicious registry
|
|
145
|
+
* responses. Only digits, dots, dashes and alphanumerics allowed (semver prerelease).
|
|
146
|
+
* Example: "1.2.3", "1.2.3-beta.1", "2.0.0-rc.9" — yes. "1; rm -rf ~" — no.
|
|
147
|
+
*/
|
|
148
|
+
function isSafeVersion(v) {
|
|
149
|
+
return typeof v === 'string' && /^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/.test(v);
|
|
150
|
+
}
|
|
151
|
+
|
|
143
152
|
/**
|
|
144
153
|
* Re-exec the current command with the latest version.
|
|
145
154
|
*/
|
|
146
155
|
function reExec(current, latest, originalArgs) {
|
|
156
|
+
// Defensive: reject anything that doesn't look like a real semver so a
|
|
157
|
+
// compromised npm registry response can't inject shell metacharacters.
|
|
158
|
+
if (!isSafeVersion(latest)) {
|
|
159
|
+
return; // silently skip — never block CLI on update check
|
|
160
|
+
}
|
|
147
161
|
console.log(`\n⬆ Updating magector: v${current} → v${latest}...\n`);
|
|
148
162
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
// execFileSync with an argv array (no shell) — originalArgs are passed as
|
|
164
|
+
// individual argv entries, so spaces/metachars in them can't expand.
|
|
165
|
+
execFileSync('npx', ['-y', `magector@${latest}`, ...originalArgs], {
|
|
151
166
|
stdio: 'inherit',
|
|
152
167
|
env: { ...process.env, MAGECTOR_NO_UPDATE: '1' }
|
|
153
168
|
});
|