security-mcp 1.0.2 → 1.0.4

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 security-mcp contributors
3
+ Copyright (c) 2024-2026 security-mcp contributors
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,7 +1,17 @@
1
1
  import { runPrGate } from "../gate/policy.js";
2
+ // Allowlist refs to the same safe character set enforced in diff.ts. CWE-88.
3
+ const SAFE_REF_RE = /^[a-zA-Z0-9_.\-/]+$/;
4
+ function safeEnvRef(envVar, defaultValue) {
5
+ const val = process.env[envVar] || defaultValue;
6
+ if (!SAFE_REF_RE.test(val)) {
7
+ console.error(`Invalid value for ${envVar}: "${val}". Using default: "${defaultValue}".`);
8
+ return defaultValue;
9
+ }
10
+ return val;
11
+ }
2
12
  async function main() {
3
- const baseRef = process.env.SECURITY_GATE_BASE_REF || "origin/main";
4
- const headRef = process.env.SECURITY_GATE_HEAD_REF || "HEAD";
13
+ const baseRef = safeEnvRef("SECURITY_GATE_BASE_REF", "origin/main");
14
+ const headRef = safeEnvRef("SECURITY_GATE_HEAD_REF", "HEAD");
5
15
  const policyPath = process.env.SECURITY_GATE_POLICY || ".mcp/policies/security-policy.json";
6
16
  const result = await runPrGate({ baseRef, headRef, policyPath });
7
17
  // Print result for Actions logs
package/dist/cli/index.js CHANGED
File without changes
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Auto-detects installed editors and writes MCP server config + Claude Code skill.
5
5
  */
6
- import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "fs";
7
- import { dirname, join, resolve } from "path";
8
- import { homedir, platform } from "os";
9
- import { fileURLToPath } from "url";
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from "node:fs";
7
+ import { dirname, join, resolve } from "node:path";
8
+ import { homedir, platform } from "node:os";
9
+ import { fileURLToPath } from "node:url";
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  const PKG_ROOT = resolve(__dirname, "../..");
12
12
  const MCP_ENTRY = {
13
+ type: "stdio",
13
14
  command: "npx",
14
15
  args: ["-y", "security-mcp", "serve"]
15
16
  };
@@ -27,7 +28,7 @@ function getVsCodeSettingsPath() {
27
28
  return join(homedir(), ".config", "Code", "User", "settings.json");
28
29
  }
29
30
  function getEditorTargets(opts) {
30
- const claudeCodePath = resolveHome("~/.claude/settings.json");
31
+ const claudeCodePath = resolveHome("~/.claude.json");
31
32
  const cursorGlobalPath = resolveHome("~/.cursor/mcp.json");
32
33
  const cursorLocalPath = ".cursor/mcp.json";
33
34
  const vscodePath = getVsCodeSettingsPath();
@@ -4,16 +4,23 @@ export async function checkSecrets(_) {
4
4
  const findings = [];
5
5
  const patterns = [
6
6
  "-----BEGIN PRIVATE KEY-----",
7
- "AKIA", // AWS
7
+ "AKIA", // AWS access key prefix
8
8
  "AIza", // Google API key prefix
9
9
  "xoxb-", // Slack bot token
10
- "sk-", // common LLM key prefix
10
+ "sk-", // common LLM key prefix (OpenAI etc.)
11
11
  "SECRET_KEY",
12
12
  "PRIVATE_KEY"
13
13
  ];
14
- const { stdout } = await execa("git", ["grep", "-n", "--untracked", "--no-index", "-I", "-e", patterns.join("|"), "."], {
15
- reject: false
16
- });
14
+ // Each pattern must be passed as a separate -e flag.
15
+ // Joining with | and using a single -e flag relies on ERE alternation, but
16
+ // git grep defaults to BRE where | is a literal character, not alternation —
17
+ // causing all patterns to be silently missed (false-negative). CWE-688.
18
+ const eFlags = patterns.flatMap((p) => ["-e", p]);
19
+ const { stdout } = await execa("git",
20
+ // --no-index: search working tree without git index (covers untracked files)
21
+ // -l: list files with matches (reduce noise); -n: show line numbers
22
+ // -I: skip binary files
23
+ ["grep", "-n", "--no-index", "-I", ...eFlags, "."], { reject: false });
17
24
  if (stdout.trim()) {
18
25
  findings.push({
19
26
  id: "POSSIBLE_SECRET",
package/dist/gate/diff.js CHANGED
@@ -1,5 +1,15 @@
1
1
  import { execa } from "execa";
2
+ // Allowlist for git ref strings. Blocks option injection (e.g. --upload-pack=…)
3
+ // and git pathspec magic characters. CWE-88 / MITRE ATT&CK T1059.
4
+ const SAFE_REF_RE = /^[a-zA-Z0-9_.\-/]+$/;
5
+ function validateRef(name, value) {
6
+ if (!value || !SAFE_REF_RE.test(value)) {
7
+ throw new Error(`Invalid git ref for ${name}: must contain only alphanumerics, _, ., -, /`);
8
+ }
9
+ }
2
10
  export async function getChangedFiles(opts) {
11
+ validateRef("baseRef", opts.baseRef);
12
+ validateRef("headRef", opts.headRef);
3
13
  // Uses git diff in CI. Assumes checkout has full history for baseRef.
4
14
  const { stdout } = await execa("git", ["diff", "--name-only", `${opts.baseRef}...${opts.headRef}`], {
5
15
  stdio: ["ignore", "pipe", "pipe"]
@@ -1,8 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { readFileSync, existsSync } from "fs";
4
- import { dirname, join, resolve } from "path";
5
- import { fileURLToPath } from "url";
3
+ import { readFileSync, existsSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
6
  import { z } from "zod";
7
7
  import { runPrGate } from "../gate/policy.js";
8
8
  import { readFileSafe } from "../repo/fs.js";
@@ -33,6 +33,22 @@ function asTextResponse(data) {
33
33
  const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
34
34
  return { content: [{ type: "text", text }] };
35
35
  }
36
+ /**
37
+ * Wraps a tool handler so that unhandled exceptions never leak internal paths,
38
+ * stack traces, or system details back to the MCP caller. CWE-209.
39
+ */
40
+ function safeTool(handler) {
41
+ return async (args, extra) => {
42
+ try {
43
+ return await handler(args, extra);
44
+ }
45
+ catch (err) {
46
+ // Return only the sanitized message — never the stack or internal path.
47
+ const msg = err instanceof Error ? err.message : "An internal error occurred";
48
+ return asTextResponse(`[security-mcp error] ${msg}`);
49
+ }
50
+ };
51
+ }
36
52
  // ---------------------------------------------------------------------------
37
53
  // Existing tools (unchanged)
38
54
  // ---------------------------------------------------------------------------
@@ -42,7 +58,7 @@ const RunPrGateParams = {
42
58
  policyPath: z.string().optional().describe("Override policy path. Default: .mcp/policies/security-policy.json")
43
59
  };
44
60
  const RunPrGateSchema = z.object(RunPrGateParams);
45
- tool("security.run_pr_gate", "Run the security policy gate against the current workspace. Returns PASS/FAIL plus findings and required actions.", RunPrGateParams, async (args, _extra) => {
61
+ tool("security.run_pr_gate", "Run the security policy gate against the current workspace. Returns PASS/FAIL plus findings and required actions.", RunPrGateParams, safeTool(async (args, _extra) => {
46
62
  const { baseRef, headRef, policyPath } = RunPrGateSchema.parse(args);
47
63
  const result = await runPrGate({
48
64
  baseRef,
@@ -50,27 +66,27 @@ tool("security.run_pr_gate", "Run the security policy gate against the current w
50
66
  policyPath: policyPath ?? ".mcp/policies/security-policy.json"
51
67
  });
52
68
  return asTextResponse(result);
53
- });
69
+ }));
54
70
  const ReadFileParams = {
55
71
  path: z.string().describe("Relative path in the repo.")
56
72
  };
57
73
  const ReadFileSchema = z.object(ReadFileParams);
58
- tool("repo.read_file", "Read a file from the repo workspace.", ReadFileParams, async (args, _extra) => {
74
+ tool("repo.read_file", "Read a file from the repo workspace.", ReadFileParams, safeTool(async (args, _extra) => {
59
75
  const { path } = ReadFileSchema.parse(args);
60
76
  const data = await readFileSafe(path);
61
77
  return asTextResponse(data);
62
- });
78
+ }));
63
79
  const SearchParams = {
64
80
  query: z.string().describe("Plain string or regex pattern."),
65
81
  isRegex: z.boolean().optional().describe("Treat query as regex. Default false."),
66
82
  maxMatches: z.number().int().min(1).max(500).optional().describe("Default 200.")
67
83
  };
68
84
  const SearchSchema = z.object(SearchParams);
69
- tool("repo.search", "Search the repo for a regex or string. Returns matches with file + line numbers.", SearchParams, async (args, _extra) => {
85
+ tool("repo.search", "Search the repo for a regex or string. Returns matches with file + line numbers.", SearchParams, safeTool(async (args, _extra) => {
70
86
  const { query, isRegex, maxMatches } = SearchSchema.parse(args);
71
87
  const matches = await searchRepo({ query, isRegex: !!isRegex, maxMatches: maxMatches ?? 200 });
72
88
  return asTextResponse(matches);
73
- });
89
+ }));
74
90
  // ---------------------------------------------------------------------------
75
91
  // New tool: security.get_system_prompt
76
92
  // ---------------------------------------------------------------------------
@@ -81,7 +97,7 @@ const GetSystemPromptParams = {
81
97
  payment_processor: z.string().optional().describe("Payment processor in use, e.g. 'Stripe', 'Braintree', 'Adyen', or 'none'.")
82
98
  };
83
99
  const GetSystemPromptSchema = z.object(GetSystemPromptParams);
84
- tool("security.get_system_prompt", "Return the full security engineering system prompt. Optionally customized with your stack, cloud provider, and payment processor. Use this as the system prompt to configure Claude as an elite security engineer for your project.", GetSystemPromptParams, async (args, _extra) => {
100
+ tool("security.get_system_prompt", "Return the full security engineering system prompt. Optionally customized with your stack, cloud provider, and payment processor. Use this as the system prompt to configure Claude as an elite security engineer for your project.", GetSystemPromptParams, safeTool(async (args, _extra) => {
85
101
  const { stack, cloud, payment_processor } = GetSystemPromptSchema.parse(args);
86
102
  let prompt = SECURITY_PROMPT;
87
103
  // Append a project-specific scope section if any context was provided
@@ -103,7 +119,7 @@ tool("security.get_system_prompt", "Return the full security engineering system
103
119
  prompt = prompt + scopeLines.join("\n");
104
120
  }
105
121
  return asTextResponse(prompt);
106
- });
122
+ }));
107
123
  // ---------------------------------------------------------------------------
108
124
  // New tool: security.threat_model
109
125
  // ---------------------------------------------------------------------------
@@ -113,7 +129,7 @@ const ThreatModelParams = {
113
129
  surfaces: z.array(z.enum(["web", "api", "mobile", "ai", "infra", "data"])).optional().describe("Attack surfaces involved. Defaults to all.")
114
130
  };
115
131
  const ThreatModelSchema = z.object(ThreatModelParams);
116
- tool("security.threat_model", "Generate a STRIDE + PASTA + ATT&CK threat model template for a described feature or component. Returns a structured Markdown document ready to fill in.", ThreatModelParams, async (args, _extra) => {
132
+ tool("security.threat_model", "Generate a STRIDE + PASTA + ATT&CK threat model template for a described feature or component. Returns a structured Markdown document ready to fill in.", ThreatModelParams, safeTool(async (args, _extra) => {
117
133
  const { feature, surfaces } = ThreatModelSchema.parse(args);
118
134
  const surfaceList = surfaces ?? ["web", "api", "mobile", "ai", "infra", "data"];
119
135
  const template = `# Threat Model: ${feature}
@@ -226,7 +242,7 @@ Describe Level 0 (context) and Level 1 (process) flows in prose or embed a diagr
226
242
  - [ ] Compliance requirements addressed and documented
227
243
  `;
228
244
  return asTextResponse(template);
229
- });
245
+ }));
230
246
  // ---------------------------------------------------------------------------
231
247
  // New tool: security.checklist
232
248
  // ---------------------------------------------------------------------------
@@ -317,7 +333,7 @@ Use before every production release. All items must be checked or explicitly ris
317
333
  - [ ] Payment-adjacent systems network-segmented from non-payment systems
318
334
  - [ ] Audit trail maintained for all payment operations
319
335
  `;
320
- tool("security.checklist", "Return the pre-release security checklist, optionally filtered by attack surface (web, api, mobile, ai, infra, payments, all).", ChecklistParams, async (args, _extra) => {
336
+ tool("security.checklist", "Return the pre-release security checklist, optionally filtered by attack surface (web, api, mobile, ai, infra, payments, all).", ChecklistParams, safeTool(async (args, _extra) => {
321
337
  const { surface } = ChecklistSchema.parse(args);
322
338
  if (!surface || surface === "all") {
323
339
  return asTextResponse(CHECKLIST_ALL);
@@ -343,7 +359,7 @@ tool("security.checklist", "Return the pre-release security checklist, optionall
343
359
  const sectionEnd = lines.findIndex((l, i) => i > start + 1 && l.startsWith("## "));
344
360
  const section = lines.slice(start, sectionEnd === -1 ? undefined : sectionEnd).join("\n");
345
361
  return asTextResponse(`# Pre-Release Security Checklist (${surface})\n\n${allSurfaces}\n\n${section}`);
346
- });
362
+ }));
347
363
  // ---------------------------------------------------------------------------
348
364
  // New tool: security.generate_policy
349
365
  // ---------------------------------------------------------------------------
@@ -353,7 +369,7 @@ const GeneratePolicyParams = {
353
369
  .describe("Primary cloud provider. Adjusts cloud-specific evidence expectations.")
354
370
  };
355
371
  const GeneratePolicySchema = z.object(GeneratePolicyParams);
356
- tool("security.generate_policy", "Generate a security-policy.json for your project based on your active surfaces and cloud provider. Save the output to .mcp/policies/security-policy.json.", GeneratePolicyParams, async (args, _extra) => {
372
+ tool("security.generate_policy", "Generate a security-policy.json for your project based on your active surfaces and cloud provider. Save the output to .mcp/policies/security-policy.json.", GeneratePolicyParams, safeTool(async (args, _extra) => {
357
373
  const { surfaces, cloud } = GeneratePolicySchema.parse(args);
358
374
  const activeSurfaces = surfaces ?? ["web", "api", "infra"];
359
375
  const requirements = [
@@ -416,7 +432,7 @@ tool("security.generate_policy", "Generate a security-policy.json for your proje
416
432
  const comment = "// Save this to .mcp/policies/security-policy.json and customize as needed.\n" +
417
433
  "// See https://github.com/AbrahamOO/security-mcp for full documentation.\n\n";
418
434
  return asTextResponse(comment + JSON.stringify(policy, null, 2));
419
- });
435
+ }));
420
436
  // ---------------------------------------------------------------------------
421
437
  // MCP Prompts capability
422
438
  // ---------------------------------------------------------------------------
package/dist/repo/fs.js CHANGED
@@ -1,9 +1,15 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  const ROOT = process.cwd();
4
+ // ROOT_PREFIX ensures /home/u/project-adjacent doesn't pass a startsWith check for /home/u/project
5
+ const ROOT_PREFIX = ROOT.endsWith(path.sep) ? ROOT : ROOT + path.sep;
4
6
  export async function readFileSafe(relPath) {
5
7
  const p = path.resolve(ROOT, relPath);
6
- if (!p.startsWith(ROOT))
8
+ // Allow exact match to ROOT itself or any path strictly under it.
9
+ // Using ROOT_PREFIX prevents the classic prefix-collision bypass
10
+ // (e.g. /app-sibling matching /app as a prefix). CWE-22.
11
+ if (p !== ROOT && !p.startsWith(ROOT_PREFIX)) {
7
12
  throw new Error("Path traversal blocked");
13
+ }
8
14
  return await readFile(p, "utf8");
9
15
  }
@@ -1,5 +1,26 @@
1
1
  import fg from "fast-glob";
2
2
  import { readFileSafe } from "./fs.js";
3
+ // Maximum allowed regex pattern length. Longer patterns significantly raise
4
+ // the risk of catastrophic backtracking (ReDoS). CWE-1333.
5
+ const MAX_REGEX_LEN = 256;
6
+ // Detects nested quantifiers — the most common ReDoS trigger — without being
7
+ // overly complex itself. Matches patterns like (a+)+, (a*)*, (\w+)+.
8
+ const NESTED_QUANTIFIER_RE = /\([^)]*[+*][^)]*\)[+*?{]/;
9
+ /**
10
+ * Validates and compiles a user-supplied regex string.
11
+ * Throws if the pattern is dangerously long, contains known ReDoS signatures,
12
+ * or is syntactically invalid. Returns the compiled RegExp on success.
13
+ * CWE-1333 / MITRE ATT&CK T1499 (resource exhaustion via ReDoS).
14
+ */
15
+ function compileUserRegex(pattern) {
16
+ if (pattern.length > MAX_REGEX_LEN) {
17
+ throw new Error(`Regex pattern too long (max ${MAX_REGEX_LEN} chars)`);
18
+ }
19
+ if (NESTED_QUANTIFIER_RE.test(pattern)) {
20
+ throw new Error("Regex pattern contains nested quantifiers that risk catastrophic backtracking (ReDoS)");
21
+ }
22
+ return new RegExp(pattern, "i"); // throws SyntaxError on invalid patterns
23
+ }
3
24
  const MAX_PREVIEW_LEN = 240;
4
25
  function isHit(line, query, re) {
5
26
  return re ? re.test(line) : line.includes(query);
@@ -23,7 +44,7 @@ export async function searchRepo(opts) {
23
44
  dot: true,
24
45
  ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
25
46
  });
26
- const re = opts.isRegex ? new RegExp(opts.query, "i") : null;
47
+ const re = opts.isRegex ? compileUserRegex(opts.query) : null;
27
48
  const matches = [];
28
49
  for (const file of files) {
29
50
  if (matches.length >= opts.maxMatches)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "security-mcp",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "AI security MCP server and enforcement gate for Claude Code, Cursor, GitHub Copilot, Codex, Replit, and any MCP-compatible editor. Applies OWASP, MITRE ATT&CK, NIST, Zero Trust, PCI DSS, SOC 2, and ISO 27001.",
5
5
  "type": "module",
6
6
  "license": "MIT",