Back to skill

Security audit

Ber Clawhub V060

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent purpose, but its helper can persist lessons that were not approved and can be tricked into writing outside the project boundary.

Review before installing. This skill is not trying to hide its persistence model, but it can modify durable agent instruction files; only run promotion commands for lessons you have explicitly reviewed, avoid sensitive lesson text, and do not use symlinked promotion or eval targets. Maintainers should add lifecycle checks and canonical path validation before broad use.

Vulnerability Patterns
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T02 · Agent Memory Poisoning

Error
Location
scripts/ber.js:645
Finding
Rejected, Quarantined, Superseded, or Unreviewed Lessons Can Be Promoted into Durable Agent Instructions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ber.js:645-714` **Vulnerability Type**: Missing lifecycle-state authorization for durable instruction promotion **Risk Level**: High ### Vulnerable Code ```js function cmdCard(opts) { const id = opts._[0]; const targetType = opts.to; if (!PROMOTION_TARGETS.has(targetType)) { throw new Error(`--to must be one of: ${Array.from(PROMOTION_TARGETS).join(", ")}`); } const { targetPath, rel } = targetPathFor(opts.target); validatePromotionTarget(targetType, rel); const lessons = readJsonl(LESSONS_FILE); const lesson = findLesson(lessons, id); const rendered = promotionBlock(lesson, targetType, opts.note || ""); const scan = scanPromotion(lesson, targetType, rel, rendered); const targetHash = hashFile(targetPath); const plan = { targetType, target: rel, targetHash, scan, cardPath: path.relative(process.cwd(), cardPath(lesson.id)), createdAt: new Date().toISOString(), }; lesson.promotionPlan = plan; fs.writeFileSync(cardPath(lesson.id), lessonCardMarkdown(lesson, targetType, rel, targetHash, scan, opts.note || ""), "utf8"); writeJsonl(LESSONS_FILE, lessons); console.log(`# Lesson card written\n\n- ID: ${lesson.id}\n- Card: ${plan.cardPath}\n- To: ${targetType}\n- Target: ${rel}\n- Target SHA-256: ${targetHash}\n${scanLines(scan)}`); } function cmdPromote(opts) { const id = opts._[0]; const targetType = opts.to; if (!PROMOTION_TARGETS.has(targetType)) { throw new Error(`--to must be one of: ${Array.from(PROMOTION_TARGETS).join(", ")}`); } const { targetPath, rel } = targetPathFor(opts.target); validatePromotionTarget(targetType, rel); const lessons = readJsonl(LESSONS_FILE); const lesson = findLesson(lessons, id); const rendered = promotionBlock(lesson, targetType, opts.note || ""); const scan = scanPromotion(lesson, targetType, rel, rendered); if (scan.hard.length) { throw new Error(`Promotion blocked by BER scanner ...[truncated 4030 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require an explicitly accepted and unexpired lesson before card creation and promotion: ```js function assertPromotableLesson(lesson) { if (lesson.status !== "accepted") { throw new Error(`Only accepted lessons may be promoted; current status: ${lesson.status}`); } if (isExpired(lesson)) { throw new Error("Expired lessons cannot be promoted."); } if (lesson.supersededBy) { throw new Error("Superseded lessons cannot be promoted."); } } ``` 2. Call this validation from both `cmdCard()` and `cmdPromote()`. 3. Store a separate approval record rather than treating card creation as implicit authorization. Bind the approval to: - Lesson ID - Hash of the complete lesson content - Target type - Canonical target path - Target file hash - Reviewer identity or trusted approval source - Approval timestamp 4. Revalidate the lesson hash, status, expiry, and approval immediately before writing the target. 5. Invalidate existing promotion plans whenever a lesson is rejected, quarantined, superseded, edited, or expires. 6. Add regression tests proving that proposed, rejected, quarantined, superseded, and expired lessons cannot be carded or promoted. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ber.js:229
Finding
Lexical Path Validation Allows Symlink-Based Writes Outside the Project<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ber.js:229-237`, `scripts/ber.js:677-703`, and `scripts/ber.js:736-766` **Vulnerability Type**: Symlink traversal and insufficient canonical-path validation **Risk Level**: Medium ### Vulnerable Code ```js function targetPathFor(target) { if (!target) throw new Error("--target is required"); const targetPath = path.resolve(process.cwd(), target); const rel = path.relative(process.cwd(), targetPath); if (rel.startsWith("..") || path.isAbsolute(rel)) { throw new Error(`Target must stay inside the current project: ${target}`); } if (!fs.existsSync(targetPath)) throw new Error(`Target file does not exist: ${target}`); return { targetPath, rel }; } ``` The promotion sink follows the validated path without rejecting symbolic links: ```js function cmdPromote(opts) { const id = opts._[0]; const targetType = opts.to; if (!PROMOTION_TARGETS.has(targetType)) { throw new Error(`--to must be one of: ${Array.from(PROMOTION_TARGETS).join(", ")}`); } const { targetPath, rel } = targetPathFor(opts.target); validatePromotionTarget(targetType, rel); const lessons = readJsonl(LESSONS_FILE); const lesson = findLesson(lessons, id); const rendered = promotionBlock(lesson, targetType, opts.note || ""); const scan = scanPromotion(lesson, targetType, rel, rendered); if (scan.hard.length) { throw new Error(`Promotion blocked by BER scanner: ${scan.hard.join(", ")}`); } if (scan.warnings.length) { throw new Error(`Promotion needs review: ${scan.warnings.join(", ")}. Adjust or quarantine the lesson, then write a fresh card.`); } const plan = lesson.promotionPlan; if (!plan) { throw new Error("Promotion requires a lesson card. Run card first."); } if (plan.targetType !== targetType || plan.target !== rel) { throw new Error(`Promotion card target mismatch. Card is for ${plan.targetType}:${plan.target}; requested ${targetType}:${rel}. Re-run card before ...[truncated 5767 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Canonicalize the project root and all existing targets: ```js const root = fs.realpathSync(process.cwd()); const canonicalTarget = fs.realpathSync(targetPath); const rel = path.relative(root, canonicalTarget); if (rel.startsWith("..") || path.isAbsolute(rel)) { throw new Error("Resolved target must stay inside the current project."); } ``` 2. Explicitly reject symbolic-link targets: ```js const stat = fs.lstatSync(targetPath); if (stat.isSymbolicLink()) { throw new Error("Symbolic-link targets are not allowed."); } ``` 3. Validate every parent directory of new eval targets with `lstatSync()` and `realpathSync()` to prevent traversal through a symlinked parent. 4. For newly created files, use secure creation semantics such as `O_CREAT | O_EXCL | O_NOFOLLOW` where supported. 5. Revalidate the canonical path immediately before the final append or write to reduce time-of-check/time-of-use exposure. 6. Open the target once with no-follow semantics, then hash and modify the same file descriptor rather than reopening the path. 7. Add regression tests covering: - A symlinked `memory/*.md` target - A symlinked `SKILL.md` - A symlinked eval target - A symlinked `tests/`, `evals/`, or `memory/` parent directory - A symlink swapped between card creation and promotion ]]>
Vulnerability Patterns
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (5)

Ae1

High
Category
analysis-evasion
Content
- Skill promotions must target `SKILL.md` in the current skill project.
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Vague Triggers

Medium
Confidence
91% confidence
Finding
The example exposes a bare "/ber report" trigger without indicating what data it reports on, what scope it uses, or whether confirmation is required before surfacing stored lessons or local evidence. In a skill centered on capturing and promoting behavioral lessons, an underspecified report command can cause overbroad disclosure of internal memory, project-local artifacts, or prior corrections if the agent interprets the request too broadly.

Rp1

Medium
Category
MCP Rug Pull
Confidence
94% confidence
Finding
The command uses `npx --yes svg-term-cli` without pinning an exact package version, so it may fetch and execute whatever version is current at runtime. That creates a supply-chain risk: a compromised upstream release, dependency takeover, or unexpected breaking change could lead to arbitrary code execution on the operator's machine when following the documented demo steps.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The script creates a temporary working directory and deletes it on exit via `rm -rf`, but there is no confirmation prompt, warning comment, or user-facing disclosure explaining that filesystem changes and cleanup will occur. It also later writes demo files, and this file itself does not document those side effects.

Missing User Warnings

Low
Confidence
84% confidence
Finding
The script writes `direct.md` and later `memory/decisions.md` as part of the demo, but there is no explicit warning or comment in the file that local files will be created. Although the operations are part of the demo flow, the lack of direct disclosure means users may not realize the script modifies the working directory.

Static analysis

No suspicious patterns detected.