security: scan SKILL.md and HEARTBEAT.md for injection patterns
- workspace.ts: call scanSource() on SKILL.md content during loadSkillEntries, warn on critical findings (dangerous-exec, dynamic-code-execution, etc.) - heartbeat-runner.ts: call detectSuspiciousPatterns() on HEARTBEAT.md content before injecting into agent prompt, warn on suspicious patterns Both checks are warn-only (non-blocking), purely additive. Fixes #21 (detectSkillPatterns wired into load flow) Fixes #17 (HEARTBEAT.md injection scan)
This commit is contained in:
parent
30718d612a
commit
21b74f7bdd
@ -20,6 +20,7 @@ import {
|
||||
} from "./frontmatter.js";
|
||||
import { resolvePluginSkillDirs } from "./plugin-skills.js";
|
||||
import { serializeByKey } from "./serialize.js";
|
||||
import { scanSource } from "../../security/skill-scanner.js";
|
||||
import type {
|
||||
ParsedSkillFrontmatter,
|
||||
SkillEligibilityContext,
|
||||
@ -391,6 +392,12 @@ function loadSkillEntries(
|
||||
let frontmatter: ParsedSkillFrontmatter = {};
|
||||
try {
|
||||
const raw = fs.readFileSync(skill.filePath, "utf-8");
|
||||
// SECURITY: scan skill source for malicious patterns before loading
|
||||
const findings = scanSource(raw, skill.filePath);
|
||||
const critical = findings.filter((f) => f.severity === "critical");
|
||||
if (critical.length > 0) {
|
||||
skillsLogger.warn(`[security] Skill "${skill.name}" has ${critical.length} critical finding(s): ${critical.map((f) => f.ruleId).join(", ")}`);
|
||||
}
|
||||
frontmatter = parseFrontmatter(raw);
|
||||
} catch {
|
||||
// ignore malformed skills
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
} from "../auto-reply/heartbeat.js";
|
||||
import { getReplyFromConfig } from "../auto-reply/reply.js";
|
||||
import { HEARTBEAT_TOKEN } from "../auto-reply/tokens.js";
|
||||
import { detectSuspiciousPatterns } from "../security/external-content.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import { getChannelPlugin } from "../channels/plugins/index.js";
|
||||
import type { ChannelHeartbeatDeps } from "../channels/plugins/types.js";
|
||||
@ -535,6 +536,13 @@ async function resolveHeartbeatPreflight(params: {
|
||||
const heartbeatFilePath = path.join(workspaceDir, DEFAULT_HEARTBEAT_FILENAME);
|
||||
try {
|
||||
const heartbeatFileContent = await fs.readFile(heartbeatFilePath, "utf-8");
|
||||
// SECURITY: scan HEARTBEAT.md for injection patterns before injecting into agent prompt
|
||||
const suspiciousPatterns = detectSuspiciousPatterns(heartbeatFileContent);
|
||||
if (suspiciousPatterns.length > 0) {
|
||||
log.warn(
|
||||
`[security] Suspicious patterns detected in HEARTBEAT.md (patterns=${suspiciousPatterns.length}): ${suspiciousPatterns.slice(0, 3).join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (isHeartbeatContentEffectivelyEmpty(heartbeatFileContent)) {
|
||||
return {
|
||||
...basePreflight,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user