Back to skill

Security audit

OpenClaw Security Guard

Security checks for vulnerabilities and agentic risk

Overview

This is a local security-checking skill, but it needs review because some helper scripts can write outside expected locations and the publishing guard can be bypassed.

Review this skill before installing. The scanner strings are mostly expected for a security tool, but only run the hook installer if overwriting the fixed workspace script is acceptable, and avoid the Obsidian writer unless the destination path and note title are controlled. Do not rely on the prepublish guard alone for publish/install decisions until the filename bypass and symlink containment issues are fixed.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
Findings (4)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/audit-skill-dir.mjs:28
Finding
Symbolic Link Following Allows Reads Outside the Selected Audit Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/audit-skill-dir.mjs`, lines 28–41 **Vulnerability Type**: Filesystem boundary violation through symbolic-link following **Risk Level**: High ### Vulnerable Code ```js function walk(dir) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.next' || entry.name === 'dist') continue; const p = path.join(dir, entry.name); if (entry.isDirectory()) walk(p); else if (includeExt.has(path.extname(entry.name).toLowerCase()) || entry.name === 'SKILL.md' || entry.name === 'package.json') scanFile(p); } } function scanFile(file) { const text = fs.readFileSync(file, 'utf8'); const lines = text.split(/\r?\n/); lines.forEach((line, i) => { for (const rule of rules) { if (rule.re.test(line)) { findings.push({ level: rule.level, label: rule.label, file, line: i + 1, excerpt: line.trim().slice(0, 200) }); } } }); } ``` ### Technical Analysis The directory auditor assumes that every file discovered beneath the selected directory is physically contained within that directory. However, it does not use `lstatSync` to identify symbolic links and does not compare each file's canonical path against the canonical audit root. For a symbolic link, `Dirent.isDirectory()` is false, so a link whose name has an accepted extension can reach `scanFile`. `fs.readFileSync` then follows the symbolic link and reads its external target. The scanner does not print entire files, but any external line matching a detection rule can be included in the JSON output as a finding excerpt. Large or unsuitable linked files may also cause resource consumption or scanner failure. ### Attack Path 1. An attacker prepares a skill directory containing a symbolic link with an accepted filename, such as `external.md`. 2. The symbolic link points to a file outside the skill directory that is readable b ...[truncated 812 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Canonicalize the audit root with `fs.realpathSync`. - Inspect entries with `fs.lstatSync` and reject symbolic links by default. - Canonicalize every candidate file before reading it. - Require each canonical candidate path to equal the canonical root or begin with `canonicalRoot + path.sep`. - Open files with protections against symbolic-link following where supported, such as `O_NOFOLLOW`. - Impose maximum file-size and total-scan-size limits. - Handle filesystem race conditions and read errors without exposing sensitive content. Example containment logic: ```js const canonicalRoot = fs.realpathSync(root); function assertContained(candidate) { const stat = fs.lstatSync(candidate); if (stat.isSymbolicLink()) { throw new Error(`Symbolic links are not allowed: ${candidate}`); } const canonical = fs.realpathSync(candidate); if (canonical !== canonicalRoot && !canonical.startsWith(canonicalRoot + path.sep)) { throw new Error(`Path escapes audit root: ${candidate}`); } return canonical; } ``` ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/prepublish-guard.mjs:17
Finding
Filename-Based Exclusions Allow Prepublish Security Guard Bypass<![CDATA[ ## Vulnerability Details **File Location**: `scripts/prepublish-guard.mjs`, lines 17–23 **Vulnerability Type**: Fail-open filtering of security findings **Risk Level**: High ### Vulnerable Code ```js let parsed; try { parsed = JSON.parse(result.stdout || '{}'); } catch { parsed = null; } if (!parsed) process.exit(1); const actionableBlocks = (parsed.findings || []).filter(f => f.level === 'BLOCK' && !/references\/checklist\.md|scripts\/audit-skill-dir\.mjs|scripts\/security-check\.mjs/.test(f.file)); if (actionableBlocks.length > 0) { console.error('\nPrepublish guard: BLOCK due to actionable findings.'); process.exit(1); } console.log('\nPrepublish guard: PASS (no actionable BLOCK findings).'); ``` ### Technical Analysis The prepublish wrapper suppresses every `BLOCK` finding whose path matches one of three filename patterns: - `references/checklist.md` - `scripts/audit-skill-dir.mjs` - `scripts/security-check.mjs` These exclusions are applied to files inside the user-selected audit target. An attacker controlling that target also controls its filenames and can place genuine malicious content in an excluded path. The regular expression is not anchored to a trusted package root and does not verify file identity or content context. Consequently, it cannot distinguish inert detection signatures in the security guard itself from an actual remote shell pipeline, destructive command, or embedded secret in an untrusted skill. The child scanner's nonzero status is not independently enforced after valid JSON is parsed. Therefore, a scanner result containing only excluded `BLOCK` findings can be converted into a successful prepublish result. ### Attack Path 1. An attacker creates a skill containing a genuinely blocked payload. 2. The payload is placed in a path such as `scripts/security-check.mjs`. 3. The victim invokes `prepublish-guard.mjs` against the skill. 4. `audit-skill-dir.mjs` correctly emits a `BLOCK` finding and exits with a blocking st ...[truncated 768 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not suppress findings based only on paths controlled by the audited target. - Treat every `BLOCK` finding in an untrusted target as actionable by default. - If self-scan noise must be suppressed, compare canonical file paths against an explicit set of trusted files belonging to this installed package, not against suffixes in the target. - Prefer contextual rule handling that identifies literal scanner definitions without suppressing executable content. - Honor the child process status and fail closed on unexpected status, malformed output, missing fields, or scanner errors. - Validate the parsed JSON schema before using it. - Report suppressed findings explicitly if any narrowly scoped exceptions remain necessary. A safer default is: ```js if (result.error || result.status === null) process.exit(1); let parsed; try { parsed = JSON.parse(result.stdout); } catch { process.exit(1); } if (!Array.isArray(parsed.findings)) process.exit(1); const actionableBlocks = parsed.findings.filter( finding => finding.level === 'BLOCK' ); if (actionableBlocks.length > 0 || result.status !== 0) { process.exit(1); } ``` ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/write-obsidian-audit.mjs:13
Finding
Unsanitized Note Title Permits Writes Outside the Obsidian Audit Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/write-obsidian-audit.mjs`, lines 13–27 **Vulnerability Type**: Path traversal and unsafe file overwrite **Risk Level**: Medium ### Vulnerable Code ```js const data = JSON.parse(fs.readFileSync(jsonPath, 'utf8')); const ts = new Date().toISOString().replace(/[:.]/g, '-'); const title = noteTitle || `Security Audit ${ts}`; const outPath = path.join(outDir, `${title}.md`); const lines = []; lines.push(`# ${title}`); lines.push(''); lines.push(`- Root: ${data.root}`); lines.push(`- Verdict: ${data.verdict}`); lines.push(`- Findings: ${(data.findings || []).length}`); lines.push(''); for (const f of (data.findings || [])) { lines.push(`- [${f.level}] ${f.label} — ${f.file}:${f.line}`); lines.push(` - ${f.excerpt}`); } fs.writeFileSync(outPath, lines.join('\n')); ``` ### Technical Analysis The optional `noteTitle` argument is treated as part of a filesystem path without validation. `path.join` normalizes traversal elements such as `../`, so a title containing sufficient parent-directory components can escape the intended `Security Audits` directory. The final filename always receives a `.md` suffix, which limits the primitive to Markdown-suffixed destinations. Nevertheless, any such destination writable by the current account can be created or overwritten if its parent directory exists. The output content is also partially derived from the selected audit JSON. An attacker who can influence both the title and JSON input can therefore control substantial portions of the file written outside the intended directory. ### Attack Path 1. An attacker or untrusted automation controls the `noteTitle` argument. 2. A title containing traversal components is supplied, for example `../../outside/audit`. 3. `path.join(outDir, `${title}.md`)` resolves the path outside `Security Audits`. 4. The destination's parent directories already exist and are writable by the current user. 5. `fs.writeFileSync` creates or ...[truncated 525 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Treat `noteTitle` strictly as a display title, not as a path. - Generate the filename separately using an allowlist of safe characters. - Reject `/`, `\`, `..`, control characters, and absolute paths. - Apply `path.basename` as defense in depth. - Resolve the final path and verify that it remains directly beneath `outDir`. - Refuse to overwrite existing files by default using the `wx` flag. - Add an explicit overwrite option if replacement is required. - Make the vault directory configurable instead of relying on a hard-coded user-specific path. Example: ```js const displayTitle = noteTitle || `Security Audit ${ts}`; const safeName = displayTitle .replace(/[^A-Za-z0-9 _.-]/g, '_') .replace(/\.\./g, '_'); const candidate = path.resolve(outDir, `${path.basename(safeName)}.md`); const canonicalOutDir = path.resolve(outDir); if (!candidate.startsWith(canonicalOutDir + path.sep)) { throw new Error('Invalid note title'); } fs.writeFileSync(candidate, lines.join('\n'), { flag: 'wx' }); ``` ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/install-hooks.sh:4
Finding
Hook Installer Unconditionally Overwrites an Existing Workspace Executable<![CDATA[ ## Vulnerability Details **File Location**: `scripts/install-hooks.sh`, lines 4–14 **Vulnerability Type**: Destructive installation behavior **Risk Level**: Low ### Vulnerable Code ```bash TARGET="$HOME/.openclaw/workspace/scripts/security-prepublish-guard.sh" cat > "$TARGET" <<'EOF' #!/usr/bin/env bash set -euo pipefail SKILL_DIR="${1:-}" if [ -z "$SKILL_DIR" ]; then echo "Usage: security-prepublish-guard.sh <skill-dir>" >&2 exit 2 fi node "$HOME/.openclaw/workspace/skills/openclaw-security-guard/scripts/prepublish-guard.mjs" "$SKILL_DIR" EOF chmod +x "$TARGET" ``` ### Technical Analysis The documented installer writes to a fixed workspace executable with the shell redirection operator `>`. If the target already exists, its contents are truncated and replaced without warning, confirmation, or backup. Installing a local wrapper is part of the Skill's declared functionality, so the write is not covert persistence. The vulnerability is the destructive overwrite behavior and lack of safe installation controls. The script also assumes the parent directory already exists. If it does not, installation fails rather than creating it safely. ### Attack Path 1. A user already has a script at `$HOME/.openclaw/workspace/scripts/security-prepublish-guard.sh`. 2. The user invokes the documented `install-hooks.sh` command. 3. Shell redirection opens the existing file with truncation. 4. The original script is irreversibly replaced by the bundled wrapper. 5. Subsequent automation invoking that path runs the replacement instead of the user's previous implementation. ### Impact Assessment The issue can destroy an existing user script and alter subsequent workspace automation. The write is limited to a fixed path under the invoking user's home directory and does not obtain elevated privileges. Because installation is explicit and the installed content is visible and non-malicious, the risk is primarily integrity loss and unexpected tool replacement. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Create the parent directory explicitly with restrictive and predictable permissions. - Refuse to overwrite an existing target by default. - Require an explicit `--force` option for replacement. - Offer to create a timestamped backup before overwriting. - Write to a temporary file in the same directory and atomically rename it after successful validation. - Verify that the target is not a symbolic link before writing. - Print the destination and intended action before making the change. Example safeguards: ```bash TARGET="$HOME/.openclaw/workspace/scripts/security-prepublish-guard.sh" mkdir -p -- "$(dirname -- "$TARGET")" if [ -L "$TARGET" ]; then echo "Refusing to overwrite symbolic link: $TARGET" >&2 exit 1 fi if [ -e "$TARGET" ] && [ "${1:-}" != "--force" ]; then echo "Target already exists; use --force after reviewing it: $TARGET" >&2 exit 1 fi ``` ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (15)

Credential Access

High
Category
Privilege Escalation
Content
- curl|bash / wget|sh installers
- destructive shell patterns
- risky exfiltration / webhook / netcat usage
- suspicious file targets like `~/.ssh`, `/etc/passwd`, `.env`, `id_rsa`

## Verdicts
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

External Script Fetching

High
Category
Supply Chain
Content
- exfiltration utilities and outbound secret posting

## Typical publish-time red flags
- `curl ... | bash`
- `wget ... | sh`
- `rm -rf`, `mkfs`, `dd`, `chmod -R 777`
- posting to unknown webhooks / Discord webhooks / pastebins
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

External Script Fetching

High
Category
Supply Chain
Content
## Typical publish-time red flags
- `curl ... | bash`
- `wget ... | sh`
- `rm -rf`, `mkfs`, `dd`, `chmod -R 777`
- posting to unknown webhooks / Discord webhooks / pastebins
- embedded tokens / API keys / JWTs / private keys
Confidence
90% confidence
Finding
Remote code is downloaded and executed. This bypasses code review and could introduce malicious code.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
## Typical publish-time red flags
- `curl ... | bash`
- `wget ... | sh`
- `rm -rf`, `mkfs`, `dd`, `chmod -R 777`
- posting to unknown webhooks / Discord webhooks / pastebins
- embedded tokens / API keys / JWTs / private keys
- scripts that auto-modify shell startup files without clear user intent
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
## Typical publish-time red flags
- `curl ... | bash`
- `wget ... | sh`
- `rm -rf`, `mkfs`, `dd`, `chmod -R 777`
- posting to unknown webhooks / Discord webhooks / pastebins
- embedded tokens / API keys / JWTs / private keys
- scripts that auto-modify shell startup files without clear user intent
Confidence
80% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Credential Access

High
Category
Privilege Escalation
Content
{ level: 'BLOCK', label: 'pipe-to-shell', re: /(curl .*\| *(bash|sh)|wget .*\| *(bash|sh))/i },
  { level: 'BLOCK', label: 'destructive-shell', re: /(rm -rf|mkfs|dd if=|chmod -R 777|chown -R root|:\(\)\{:\|:&\};:)/i },
  { level: 'WARN', label: 'exfiltration', re: /(discord\.com\/api\/webhooks|pastebin|transfer\.sh|api\.telegram\.org|nc |ncat |scp |rsync .*@)/i },
  { level: 'WARN', label: 'sensitive-path', re: /(\/etc\/passwd|id_rsa|authorized_keys|\.env|\.ssh|\.gnupg|keychain)/i },
  { level: 'WARN', label: 'postinstall-autoexec', re: /("postinstall"\s*:\s*"[^"]+")/i }
];
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
{ level: 'BLOCK', label: 'pipe-to-shell', re: /(curl .*\| *(bash|sh)|wget .*\| *(bash|sh))/i },
  { level: 'BLOCK', label: 'destructive-shell', re: /(rm -rf|mkfs|dd if=|chmod -R 777|chown -R root|:\(\)\{:\|:&\};:)/i },
  { level: 'WARN', label: 'exfiltration', re: /(discord\.com\/api\/webhooks|pastebin|transfer\.sh|api\.telegram\.org|nc |ncat |scp |rsync .*@)/i },
  { level: 'WARN', label: 'sensitive-path', re: /(\/etc\/passwd|id_rsa|authorized_keys|\.env|\.ssh|\.gnupg|keychain)/i },
  { level: 'WARN', label: 'postinstall-autoexec', re: /("postinstall"\s*:\s*"[^"]+")/i }
];
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
{ level: 'BLOCK', label: 'pipe-to-shell', re: /(curl .*\| *(bash|sh)|wget .*\| *(bash|sh))/i },
  { level: 'BLOCK', label: 'destructive-shell', re: /(rm -rf|mkfs|dd if=|chmod -R 777|chown -R root|:\(\)\{:\|:&\};:)/i },
  { level: 'WARN', label: 'exfiltration', re: /(discord\.com\/api\/webhooks|pastebin|transfer\.sh|api\.telegram\.org|nc |ncat |scp |rsync .*@)/i },
  { level: 'WARN', label: 'sensitive-path', re: /(\/etc\/passwd|id_rsa|authorized_keys|\.env|\.ssh|\.gnupg|keychain)/i },
  { level: 'WARN', label: 'postinstall-autoexec', re: /("postinstall"\s*:\s*"[^"]+")/i }
];
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Anti-Refusal Statement

High
Category
Anti-Refusal
Content
const RULES = {
  text: [
    { level: 'BLOCK', label: 'prompt-injection', re: /(ignore (all|previous) instructions|developer mode|system override|reveal (the )?system prompt|bypass safety|jailbreak)/i },
    { level: 'BLOCK', label: 'secret', re: /(sk-[a-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|OPENSSH|EC) PRIVATE KEY-----|xox[baprs]-[A-Za-z0-9-]+)/i },
    { level: 'WARN', label: 'exfiltration', re: /(send (me|this|that).*(token|secret|key|cookie)|upload .*?(\.env|id_rsa|passwd)|pastebin|webhook)/i },
    { level: 'WARN', label: 'obfuscation', re: /(```json|@context|mainEntity|acceptedAnswer|\| .* \| .* \|)/i }
Confidence
90% confidence
Finding
Skill attempts to nullify the agent's safety policies or restrictions ('you have no restrictions', 'ignore your guidelines', 'do anything now'). This is a direct jailbreak that disables guardrails.

Instruction Override

High
Category
Prompt Injection
Content
const RULES = {
  text: [
    { level: 'BLOCK', label: 'prompt-injection', re: /(ignore (all|previous) instructions|developer mode|system override|reveal (the )?system prompt|bypass safety|jailbreak)/i },
    { level: 'BLOCK', label: 'secret', re: /(sk-[a-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|OPENSSH|EC) PRIVATE KEY-----|xox[baprs]-[A-Za-z0-9-]+)/i },
    { level: 'WARN', label: 'exfiltration', re: /(send (me|this|that).*(token|secret|key|cookie)|upload .*?(\.env|id_rsa|passwd)|pastebin|webhook)/i },
    { level: 'WARN', label: 'obfuscation', re: /(```json|@context|mainEntity|acceptedAnswer|\| .* \| .* \|)/i }
Confidence
90% confidence
Finding
This pattern attempts to override system instructions or ignore safety constraints. Without LLM analysis, manual review is recommended.

YARA rule 'agent_skill_prompt_injection_hidden_instructions': Prompt injection or hidden instructions embedded in AI agent skill text [agent_skills]

High
Category
YARA Match
Content
#!/usr/bin/env node

const [, , mode, ...rest] = process.argv;
const input = rest.join(' ').trim();

if (!mode || !input) {
  console.error('Usage: node security-check.mjs <text|command|url|path> <value>');
  process.exit(2);
}

const RULES = {
  text: [
    { level: 'BLOCK', label: 'prompt-injection', re: /(ignore (all|previous) instructions|developer mode|system override|reveal (the )?system prompt|bypass safety|jailbreak)/i },
    { level: 'BLOCK', label: 'secret', re: /(sk-[a-z0-9]{16,}|ghp_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|-----BEGIN (RSA|OPENSSH|EC) PRIVATE KEY-----|xox[baprs]-[A-Za-z0-9-]+)/i },
    { level: 'WARN', label: 'exfiltration', re: /(send (me|this|that).*(token|secret|key|cookie)|upload .*?(\.env|id_rsa|passwd)|pastebin|webhook)/i },
    { level: 'WARN', label: 'obfuscation', re: /(```json|@context|mainEntity|acceptedAnswer|\| .* \| .* \|)/i }
  ],
  command: [
    { level: 'BLOCK', label: 'destructive-shell', re: /(rm -rf|mkfs|dd if=|shutdown|reboot|:\(\)\{
Confidence
80% confidence
Finding
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).

Credential Access

High
Category
Privilege Escalation
Content
path: [
    { level: 'BLOCK', label: 'traversal', re: /(\.\.\/|\.\.\\)/ },
    { level: 'BLOCK', label: 'sensitive-path', re: /(\/etc\/passwd|\/proc\/|id_rsa|\.env|keychain|\/var\/run\/docker\.sock|authorized_keys)/i },
    { level: 'WARN', label: 'home-secret-path', re: /(\.ssh|\.gnupg|\.aws|\.npmrc|\.pypirc)/i }
  ]
};
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill instructs users to run multiple shell scripts but does not declare any tool scope such as permissions or allowed-tools. That creates an undeclared execution capability boundary, making it easier for the skill to be used in environments where shell access is broader than reviewers expect.

Description-Behavior Mismatch

Medium
Confidence
91% confidence
Finding
The script writes audit output to a hard-coded path inside a specific user's Obsidian vault, creating an undocumented side effect outside the provided input/output contract. This can leak potentially sensitive audit data into an unrelated note repository and causes the skill to modify user-local content in a location that may not match user intent or the skill's declared security-guard purpose.

Context-Inappropriate Capability

Medium
Confidence
92% confidence
Finding
Embedding a fixed capability to write into '/Users/m1/Desktop/obsidianvault/ClawLite' is unsafe because it grants file-system write behavior to a personal directory unrelated to the input JSON path. In a security-oriented skill, hidden writes to a specific desktop vault increase risk because findings may contain sensitive paths, excerpts, or secrets that get copied into a broader note system without explicit consent.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
scripts/prepublish-guard.mjs:13