Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 28 additions & 5 deletions src/rules/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import type { Finding, Rule } from "../types.js";
import { containsAny, SECRET_PATH_PATTERNS } from "./helpers.js";

const PATH_ARG_NAMES = ["path", "file", "filename", "filepath", "dir", "directory", "location"];
const UNAMBIGUOUS_SECRET_FILE_PATTERNS = [
".env",
"id_rsa",
"id_ed25519",
".pem",
"/etc/passwd",
"/etc/shadow",
];

/**
* MCP030 - A resource points at secret material or a sensitive system path.
Expand All @@ -12,24 +20,39 @@ export const resourceExposesSecrets: Rule = {
id: "MCP030",
title: "Resource exposes secrets or sensitive paths",
description:
"Resources should never surface credential files, private keys, or sensitive system paths.",
"Sensitive resource URIs are critical; unambiguous secret-file references in resource metadata are lower-confidence findings.",
severity: "critical",
category: "secrets",
evaluate(target, ctx): Finding[] {
const findings: Finding[] = [];
for (const res of target.resources) {
const haystack = `${res.uri} ${res.name ?? ""} ${res.description ?? ""}`;
const hit = containsAny(haystack, SECRET_PATH_PATTERNS);
if (hit) {
const uriHit = containsAny(res.uri, SECRET_PATH_PATTERNS);
if (uriHit) {
findings.push(
ctx.report({
title: "Resource surfaces sensitive material",
message: `Resource "${res.uri}" references "${hit}", which commonly holds secrets or system credentials.`,
message: `Resource "${res.uri}" references "${uriHit}", which commonly holds secrets or system credentials.`,
remediation:
"Remove the resource or restrict it to non-sensitive content. Never expose credential files or system paths over MCP.",
location: res.uri,
}),
);
continue;
}

const metadata = `${res.name ?? ""} ${res.description ?? ""}`;
const metadataHit = containsAny(metadata, UNAMBIGUOUS_SECRET_FILE_PATTERNS);
if (metadataHit) {
findings.push(
ctx.report({
severity: "medium",
title: "Resource metadata references sensitive material",
message: `Metadata for resource "${res.uri}" references "${metadataHit}", which may identify secret material.`,
remediation:
"Verify that the resource does not expose the referenced file, and remove or restrict it if it contains sensitive material.",
location: res.uri,
}),
);
}
}
return findings;
Expand Down
48 changes: 48 additions & 0 deletions test/rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,54 @@ describe("MCP002 exec detection", () => {
});
});

describe("MCP030 resource secret detection", () => {
function findingsFor(resources: AuditTarget["resources"]) {
return audit(makeTarget({ resources })).findings.filter((f) => f.ruleId === "MCP030");
}

it("does not treat generic credentials or secrets wording as critical evidence", () => {
const findings = findingsFor([
{
uri: "docs://project-notes",
name: "Credential safety notes",
description: "Project notes. Contains no credentials or secrets.",
},
]);

expect(findings).toEqual([]);
});

it("reports an unambiguous metadata filename reference below critical severity", () => {
const findings = findingsFor([
{
uri: "docs://deployment-guide",
description: "Includes a copy of the production .env file.",
},
]);

expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({
severity: "medium",
location: "docs://deployment-guide",
});
});

it("keeps sensitive resource URIs critical", () => {
const findings = findingsFor([
{
uri: "file:///home/app/.env",
description: "Configuration reference",
},
]);

expect(findings).toHaveLength(1);
expect(findings[0]).toMatchObject({
severity: "critical",
location: "file:///home/app/.env",
});
});
});

describe("MCP040 http auth", () => {
it("flags http transport without auth", () => {
const target = makeTarget({
Expand Down
Loading