Back to skill

Security audit

OpenScan

Security checks for vulnerabilities and agentic risk

Overview

OpenScan is a coherent malware-scanning skill, but a real command-injection bug can let a specially named file run commands on macOS, so it needs review before use.

Treat this as a Review item rather than confirmed malware. Do not run the current scanner with sudo or against untrusted macOS files until the codesign check is changed to an argument-array API such as execFileSync. The YARA-style strings are expected for a malware scanner, but the command-injection and malformed-binary crash issues should be fixed before relying on it for security decisions.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
lib/scanner.js:212
Finding
Shell Command Injection Through Code-Signature Verification<![CDATA[ ## Vulnerability Details **File Location**: `lib/scanner.js:212-227` **Vulnerability Type**: OS command injection through shell interpolation **Risk Level**: High ### Complete Vulnerable Code ```javascript function checkCodeSignature(filePath) { try { execSync(`codesign --verify --deep --strict "${filePath}" 2>&1`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }); return { valid: true, signed: true }; } catch (err) { const output = err.stdout || err.stderr || ''; if (output.includes('not signed')) { return { valid: false, signed: false, error: 'Not signed' }; } return { valid: false, signed: true, error: output.trim() }; } } ``` The attacker-controlled path originates from the CLI and reaches the vulnerable function through the following logic: ```javascript const targetPath = args.find(a => !a.startsWith('--')); const fullPath = path.resolve(targetPath); results = [await scanFile(fullPath, options)]; ``` ### Technical Analysis `checkCodeSignature()` inserts `filePath` directly into a command string passed to `child_process.execSync()`. Unlike argument-based process execution, `execSync()` runs the string through a shell. Surrounding the path with double quotes does not make this safe. Shell command substitution such as `$(command)` remains active inside double quotes. A filename containing a double quote can also terminate the quoted argument and introduce shell control operators. The vulnerable branch is reached on macOS when the scanned file is a Mach-O binary or has its owner-executable permission bit set: ```javascript if (process.platform === 'darwin' && (result.type === 'macho' || isExecutable(filePath))) { result.details.codesign = checkCodeSignature(filePath); } ``` The scanner is explicitly intended to process untrusted files, so filenames and paths must be treated as hostile input. ### Attack Path 1. An attacker creates or distributes a Mach-O or executable file whose ...[truncated 1360 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Avoid invoking a shell for code-signature verification. Use an argument-array API so that the path is passed literally: ```javascript const { execFileSync } = require('child_process'); function checkCodeSignature(filePath) { try { execFileSync( 'codesign', ['--verify', '--deep', '--strict', filePath], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 30000 } ); return { valid: true, signed: true }; } catch (err) { const output = String(err.stderr || err.stdout || ''); if (output.includes('not signed')) { return { valid: false, signed: false, error: 'Not signed' }; } return { valid: false, signed: true, error: output.trim() }; } } ``` Additional hardening should include: 1. Do not set `shell: true`. 2. Add a timeout to prevent a stalled external process from blocking a scan indefinitely. 3. Test filenames containing spaces, quotes, dollar signs, backticks, semicolons, newlines, and command-substitution syntax. 4. Run the scanner as an unprivileged account and document that users should not invoke it through `sudo`. 5. Consider constraining the child process environment and resolving the trusted `codesign` executable path explicitly where appropriate. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
lib/elf.js:51
Finding
Malformed 64-bit ELF File Can Abort a Scan<![CDATA[ ## Vulnerability Details **File Location**: `lib/elf.js:51-52, 91-94` **Vulnerability Type**: Unchecked binary-header reads causing denial of service **Risk Level**: Medium ### Complete Vulnerable Code ```javascript function parseELF(buffer) { if (!isELF(buffer)) return null; if (buffer.length < 52) return null; // Minimum ELF header size const elfClass = buffer[4]; const is64 = elfClass === ELFCLASS64; const dataEncoding = buffer[5]; // 1 = little endian, 2 = big endian const isLE = dataEncoding === 1; const readU16 = isLE ? (off) => buffer.readUInt16LE(off) : (off) => buffer.readUInt16BE(off); const readU32 = isLE ? (off) => buffer.readUInt32LE(off) : (off) => buffer.readUInt32BE(off); const readU64 = isLE ? (off) => buffer.readBigUInt64LE(off) : (off) => buffer.readBigUInt64BE(off); // ... const shOff = is64 ? Number(readU64(40)) : readU32(32); const shEntSize = is64 ? readU16(58) : readU16(46); const shNum = is64 ? readU16(60) : readU16(48); const shStrIndex = is64 ? readU16(62) : readU16(50); ``` ### Technical Analysis The parser applies a single 52-byte minimum-length check to both 32-bit and 64-bit ELF files. A valid 64-bit ELF header is 64 bytes long, and the 64-bit parsing branch reads fields through byte offset 63. An attacker can provide a buffer that: - Begins with the ELF magic bytes; - Sets `EI_CLASS` to `ELFCLASS64`; - Has a total size from 52 through 63 bytes. Such a file passes the initial validation but causes `Buffer.readUInt16*()` or another header read to exceed the buffer boundary. Node.js then throws a `RangeError`. The exception is not converted into a per-file finding inside `scanFile()` or `scanDirectory()`. Consequently, one malformed file can reject the scan operation and prevent remaining files from being inspected. ### Attack Path 1. An attacker places a truncated file with valid ELF magic and a 64-bit class marker in a directory to be audited. 2. The us ...[truncated 935 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Apply format-specific header validation before any field access: ```javascript const ELF32_HEADER_SIZE = 52; const ELF64_HEADER_SIZE = 64; const elfClass = buffer[4]; if (elfClass === ELFCLASS32 && buffer.length < ELF32_HEADER_SIZE) { return null; } if (elfClass === ELFCLASS64 && buffer.length < ELF64_HEADER_SIZE) { return null; } if (elfClass !== ELFCLASS32 && elfClass !== ELFCLASS64) { return null; } ``` Further hardening should include: 1. Validate `EI_DATA` and reject unsupported byte-order values. 2. Check each table entry size against the minimum structure size before reading fields. 3. Use overflow-safe range validation for every offset and length. 4. Reject unsafe 64-bit values that cannot be represented precisely as JavaScript numbers. 5. Wrap parsing on a per-file basis so malformed input returns an `Invalid ELF structure` finding rather than terminating the directory scan. 6. Add fuzz tests and regression cases for ELF files of every truncated length from 0 through 64 bytes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
lib/macho.js:68
Finding
Truncated FAT Mach-O Header Can Abort a Scan<![CDATA[ ## Vulnerability Details **File Location**: `lib/macho.js:68-72, 99-102` **Vulnerability Type**: Unchecked binary-header read causing denial of service **Risk Level**: Medium ### Complete Vulnerable Code ```javascript function parseMachO(buffer) { if (buffer.length < 4) return null; const magicBE = buffer.readUInt32BE(0); // Check for FAT/Universal binary (always big-endian header) if (magicBE === MAGIC.FAT_MAGIC || magicBE === MAGIC.FAT_CIGAM) { return parseFatBinary(buffer); } // Check for regular Mach-O if (!isMachO(buffer)) return null; return parseSingleMachO(buffer, 0); } ``` The FAT parser then performs an additional read without first establishing that the buffer contains the full FAT header: ```javascript function parseFatBinary(buffer) { const magic = buffer.readUInt32BE(0); const nArch = buffer.readUInt32BE(4); if (nArch > 10) return null; // Sanity check ``` ### Technical Analysis `parseMachO()` only requires four bytes before recognizing a FAT Mach-O magic value and calling `parseFatBinary()`. The FAT parser immediately reads a 32-bit architecture count at offset 4, which requires a buffer of at least eight bytes. A crafted file containing only a four- to seven-byte FAT Mach-O prefix therefore passes the format check but causes `readUInt32BE(4)` to throw a `RangeError`. As with the ELF parser issue, parsing errors are not isolated on a per-file basis. A malformed FAT Mach-O can therefore terminate a single-file scan or interrupt recursive directory scanning. ### Attack Path 1. An attacker creates a four- to seven-byte file beginning with `FAT_MAGIC` or `FAT_CIGAM`. 2. The attacker places the file in a directory that a victim or automated service will scan. 3. `isMachO()` and `parseMachO()` recognize the FAT magic value. 4. `parseMachO()` calls `parseFatBinary()` without verifying that eight header bytes are available. 5. `parseFatBinary()` reads `nArch` at offset 4. 6. Node.js throws an out-of-ra ...[truncated 494 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Validate the complete FAT header before reading the architecture count: ```javascript function parseFatBinary(buffer) { const FAT_HEADER_SIZE = 8; const FAT_ARCH_SIZE = 20; if (buffer.length < FAT_HEADER_SIZE) { return null; } const magic = buffer.readUInt32BE(0); const nArch = buffer.readUInt32BE(4); if (nArch > 10) { return null; } if (FAT_HEADER_SIZE + nArch * FAT_ARCH_SIZE > buffer.length) { return null; } // Continue parsing... } ``` Additional hardening should include: 1. Verify all architecture offsets and sizes using overflow-safe range checks. 2. Validate minimum regular Mach-O header lengths before calling `parseSingleMachO()`. 3. Catch parser exceptions per file and report malformed input without aborting the whole directory scan. 4. Add regression tests for truncated FAT headers and architecture tables. 5. Fuzz both FAT and single-architecture Mach-O parsers with malformed offsets, sizes, command counts, and command lengths. ]]>
Vulnerability Patterns
  • 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
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • YARA SignaturesMalware Match, Webshell Match, Cryptominer Match
Findings (6)

YARA rule 'keylogger_indicators': Keylogger functionality in scripts or source code [malware]

High
Category
YARA Match
Content
ecutable stack/heap, missing NX
- **Packing/encryption**: High entropy detection, encrypted segments
- **Segment anomalies**: Suspicious names like `__INJECT`, `UPX`, `__MALWARE`

### Pattern Detection

Scans binary content for:
- **Shellcode patterns**: x86/x64 prologue sequences, NOP sleds, infinite loops
- **Suspicious APIs**: Process injection (CreateRemoteThread, VirtualAllocEx), keylogging (GetAsyncKeyState), anti-debugging (IsDebuggerPresent)
- **Network indicators**: Embedded URLs, IP addresses
- **Encoded payloads**: Large base64 blobs

### Script Analysis

For shell scripts, Python, JavaScript, etc.:
- **Dangerous patterns**: `curl | bash`, `eval()`, base64 decode + exec
- **Persistence mechanisms**: crontab, launchctl, LaunchAgents
- **Injection vectors**: `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`
- **Obfuscation**: Heavy hex escaping, very long lines

## Installation

```bash
# Clone the repo
git clone https://github.com/marqbritt/openscan.git
cd openscan

# No dependencies - p
Confidence
70% confidence
Finding
YARA rule matched a known malware signature (reverse shell, backdoor, ransomware, C2 framework, or info stealer).

External Script Fetching

High
Category
Supply Chain
Content
### Script Analysis

For shell scripts, Python, JavaScript, etc.:
- **Dangerous patterns**: `curl | bash`, `eval()`, base64 decode + exec
- **Persistence mechanisms**: crontab, launchctl, LaunchAgents
- **Injection vectors**: `LD_PRELOAD`, `DYLD_INSERT_LIBRARIES`
- **Obfuscation**: Heavy hex escaping, very long lines
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
{ pattern: /nc\s+-[el]/gi, name: 'netcat listener/exec' },
    { pattern: /\/dev\/tcp\//gi, name: 'bash /dev/tcp network' },
    { pattern: /rm\s+-rf\s+[\/~]/gi, name: 'dangerous rm -rf' },
    { pattern: /chmod\s+[0-7]*777/gi, name: 'chmod 777' },
    { pattern: />\s*\/etc\//gi, name: 'writing to /etc' },
    { pattern: /crontab/gi, name: 'crontab modification' },
    { pattern: /launchctl/gi, name: 'launchctl usage' },
Confidence
90% 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).

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
## Why This Exists

OpenClaw skills can declare binary dependencies via `requires.bins`. Users install these binaries from various sources (Homebrew, npm, apt, random GitHub releases). There's currently no verification that these binaries are safe.

This scanner provides:
- **Pre-trust scanning** of binaries before execution
Confidence
75% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Lp3

Medium
Category
MCP Least Privilege
Confidence
70% confidence
Finding
Without declared permissions the skill's intent is opaque and cannot be validated.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
{ pattern: /nc\s+-[el]/gi, name: 'netcat listener/exec' },
    { pattern: /\/dev\/tcp\//gi, name: 'bash /dev/tcp network' },
    { pattern: /rm\s+-rf\s+[\/~]/gi, name: 'dangerous rm -rf' },
    { pattern: /chmod\s+[0-7]*777/gi, name: 'chmod 777' },
    { pattern: />\s*\/etc\//gi, name: 'writing to /etc' },
    { pattern: /crontab/gi, name: 'crontab modification' },
    { pattern: /launchctl/gi, name: 'launchctl usage' },
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Static analysis

Detected: suspicious.dangerous_exec

Shell command execution detected (child_process).

Critical
Code
suspicious.dangerous_exec
Location
lib/scanner.js:205