Back to skill

Security audit

QRCode

Security checks for vulnerabilities and agentic risk

Overview

The QR code skill mostly matches its stated purpose, but its file-output controls are incorrectly scoped and can write outside the intended workspace.

Review this skill before installing. It should be fixed to write only to a real user-approved workspace or dedicated output directory, create temporary files safely, cap image sizes more conservatively, pin dependencies with a lockfile, and warn users not to put WiFi passwords, contact details, or other sensitive data into QR codes unless they intend to share it.

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 (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/generate.mjs:10
Finding
Output Root Resolves Outside the Project Boundary<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.mjs:10` **Vulnerability Type**: Incorrect output-directory trust boundary **Risk Level**: High ### Vulnerable Code ```js const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const WORKSPACE_ROOT = path.resolve(__dirname, '../../..'); ``` ### Technical Analysis The script derives its trusted output directory by ascending three levels from `scripts/generate.mjs`. In the audited layout, the script is located at: ```text /tmp/clawhub-codex-scan-v576b2egb8w4qfqmjwj50d0bms8e55pa-X9DIvx/artifact/scripts/generate.mjs ``` Resolving `../../..` from the `scripts` directory produces `/tmp`, rather than the project directory or a dedicated workspace directory. Subsequent path validation consistently trusts this incorrectly calculated value, so those checks do not restore the intended project boundary. Although `sanitizeOutputPath()` strips directory components and restricts extensions, it still permits writes to direct children of `/tmp`. This contradicts the documented claim that generated files are restricted to the workspace root. ### Attack Path 1. An attacker or untrusted caller invokes the generator with a controlled output name: ```bash node scripts/generate.mjs "attacker-controlled content" -o target.svg ``` 2. The script strips path components from `target.svg` and resolves it beneath `WORKSPACE_ROOT`. 3. Because `WORKSPACE_ROOT` resolves to `/tmp`, the resulting path is `/tmp/target.svg`. 4. If the process has sufficient filesystem permission, the script creates or replaces that file outside the project boundary. 5. The attacker can repeat this for files with one of the permitted extensions: `.svg`, `.png`, `.jpg`, or `.jpeg`. ### Impact Assessment The vulnerability grants write access beyond the project’s legitimate output directory. Its scope is limited to direct children of the incorrectly selected ancestor directory, approved ...[truncated 700 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not derive a security boundary by ascending a fixed number of parent directories. - Obtain the actual workspace path from a trusted runtime configuration value or explicitly use the project root: ```js const PROJECT_ROOT = path.resolve(__dirname, '..'); ``` - Prefer a dedicated output directory with restrictive permissions: ```js const OUTPUT_ROOT = path.join(PROJECT_ROOT, 'output'); fs.mkdirSync(OUTPUT_ROOT, { recursive: true, mode: 0o700 }); ``` - Resolve the destination against that exact directory and retain the `path.relative()` containment check. - Resolve the configured output root through `fs.realpathSync()` before trusting it, ensuring that the directory itself is not a symlink to another location. - Add a startup assertion that rejects unexpectedly broad directories such as `/`, `/tmp`, or a user home directory unless explicitly authorized. - Add automated tests that verify generated files remain inside the intended project or workspace directory for the deployed directory layout. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/generate.mjs:173
Finding
Predictable Temporary File Is Written Before Symlink Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.mjs:173-190` **Vulnerability Type**: Symlink-following temporary-file race **Risk Level**: High ### Vulnerable Code ```js const tempFile = `${filePath}.tmp.${process.pid}`; try { // Write to temp file with restrictive permissions const options = encoding ? { encoding, mode: 0o644 } : { mode: 0o644 }; fs.writeFileSync(tempFile, content, options); // Verify temp file is not a symlink (defense in depth) const tempStats = fs.lstatSync(tempFile); if (tempStats.isSymbolicLink()) { fs.unlinkSync(tempFile); exitError('Security Error: Temp file became a symlink'); } // Atomic rename operation fs.renameSync(tempFile, filePath); ``` ### Technical Analysis The temporary filename consists only of the destination path and process ID, making it predictable to another local actor. `fs.writeFileSync()` uses normal path-following behavior and is called before `lstatSync()` checks whether the temporary path is a symbolic link. If an attacker pre-creates the temporary path as a symlink, `writeFileSync()` follows the link and truncates or writes the linked target. The subsequent `lstatSync()` detects that the temporary pathname is a symlink, but the external target has already been modified. Deleting the symlink afterward cannot undo that write. The operation is therefore not protected against symlink attacks despite the post-write check. The use of an atomic rename for the final step does not protect the earlier temporary-file open. ### Attack Path 1. A local attacker identifies or predicts: - The requested output filename. - The generator process ID. - The output directory. 2. The attacker creates the expected temporary pathname as a symbolic link: ```text <output-file>.tmp.<pid> -> <attacker-selected writable target> ``` 3. The generator calls `fs.writeFileSync()` on that pathname. 4. Node follows the symbolic link and writes ...[truncated 1148 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create temporary files atomically rather than checking them after writing. - Generate a cryptographically random temporary name inside a trusted, non-shared output directory. - Open the file with exclusive creation and no-follow behavior where supported: ```js const flags = fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0); const fd = fs.openSync(tempFile, flags, 0o600); try { fs.writeFileSync(fd, content, encoding ? { encoding } : undefined); fs.fsyncSync(fd); } finally { fs.closeSync(fd); } fs.renameSync(tempFile, filePath); ``` - Use a securely randomized name, such as one generated with `crypto.randomBytes()`, rather than relying on the process ID. - Set temporary-file permissions to `0o600` unless broader access is explicitly required. - Verify the opened file through `fstat()` on the file descriptor, avoiding pathname-based validation after opening. - Use platform-specific safe replacement semantics for the final destination and reject symlink destinations immediately before replacement where required by the threat model. - Keep both temporary and final files inside a dedicated directory not writable by untrusted local users. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate.mjs:13
Finding
Permitted Image Dimensions Enable Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `scripts/generate.mjs:13-20`, `scripts/generate.mjs:322-327`, `scripts/generate.mjs:419-422` **Vulnerability Type**: Uncontrolled resource consumption **Risk Level**: Medium ### Vulnerable Code ```js const LIMITS = { TEXT: 4096, SIZE: 10000, SCALE: 10, MARGIN: 100, QUALITY: { MIN: 1, MAX: 100 }, SVG_BUFFER: 100 * 1024 * 1024 }; ``` ```js const targetSize = Math.max(1, Math.min(LIMITS.SIZE * LIMITS.SCALE, Math.floor(config.size * config.scale))); const cellSize = Math.floor(targetSize / (moduleCount + config.margin * 2)) || 1; const svgSize = cellSize * (moduleCount + config.margin * 2); const offset = config.margin * cellSize; // Validate computed sizes to prevent resource exhaustion if (svgSize > LIMITS.SIZE * LIMITS.SCALE) { exitError(`Error: Computed size too large (${svgSize}px). Reduce size, scale, or margin.`); } ``` ```js let image = sharp(svgBuffer, { density: 72, limitInputPixels: LIMITS.SIZE * LIMITS.SIZE * LIMITS.SCALE * LIMITS.SCALE }).resize(svgSize, svgSize, { fit: 'contain' }); ``` ### Technical Analysis The script accepts a maximum size of 10,000 and a maximum scale of 10. These values can combine into a final dimension approaching 100,000 pixels on each axis. The `sharp` input limit is calculated as: ```text 10,000 × 10,000 × 10 × 10 = 10,000,000,000 pixels ``` A 100,000 by 100,000 image contains ten billion pixels. Even a simple uncompressed four-byte-per-pixel representation would require approximately 40 GB before accounting for intermediate buffers, encoder state, SVG rendering, and process overhead. The existing validation only limits each final dimension to `SIZE * SCALE`. It does not impose a practical cap on total pixels, memory usage, output bytes, execution time, or concurrent conversions. The nominal resource limit is therefore too permissive to prevent denial of service. ### Attack Path 1. An untrusted caller requests raster output ...[truncated 1167 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Enforce a conservative limit on total output pixels: ```js const MAX_OUTPUT_PIXELS = 40_000_000; const outputPixels = svgSize * svgSize; if (!Number.isSafeInteger(outputPixels) || outputPixels > MAX_OUTPUT_PIXELS) { exitError('Error: Requested image exceeds the maximum pixel count'); } ``` - Pass the same conservative total-pixel threshold to `sharp` rather than allowing ten billion pixels. - Reduce the maximum final dimension to a deployment-appropriate value, such as 4,096 or 8,192 pixels. - Avoid independently large `size` and `scale` values that multiply into an unsafe final dimension. - Apply execution time, process-memory, and output-file-size limits at the worker or container level. - Limit concurrent image conversions. - Consider generating the raster image directly at the requested safe dimensions rather than building oversized intermediate representations. - Add boundary tests for maximum dimensions and verify that oversized requests fail before invoking `sharp`. ]]>
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 (7)

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The examples include QR payloads containing WiFi credentials and personal contact data, but the skill does not warn users that generated QR codes may expose secrets or personally identifiable information to anyone who scans or views them. In this context, the risk is realistic because the skill explicitly encourages rendering and sharing the generated files, which increases the chance of accidental disclosure.

Whitespace Padding

Medium
Category
Prompt Injection
Content
## Troubleshooting

| Issue               | Solution                                                                                                                   |
| ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `npm install` fails | Install build tools:`xcode-select --install` (macOS) or see [sharp install guide](https://sharp.pixelplumbing.com/install) |
| QR code won't scan  | Increase `--size`, use higher error correction (`--ec H`), or simplify styling                                             |
Confidence
70% confidence
Finding
Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Whitespace Padding

Medium
Category
Prompt Injection
Content
| QR code won't scan  | Increase `--size`, use higher error correction (`--ec H`), or simplify styling                                             |
| Colors not working  | Use hex format `#RRGGBB` (e.g., `#FF5733`), not RGB or color names                                                         |
| File too large      | Reduce `--size`, `--scale`, or increase `--quality` for JPG                                                                |
| Permission denied   | Check workspace directory write permissions                                                                                |

## Error Correction Levels Explained
Confidence
70% confidence
Finding
Large whitespace padding was detected (a block of blank lines or a long run of spaces). This can push injected instructions below or to the right of the visible area so a human reviewer never sees them while the agent still reads them. Manual review of the hidden content is recommended.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"generate": "node scripts/generate.mjs"
    },
    "dependencies": {
        "qrcode": "^1.5.1",
        "sharp": "^0.32.4"
    },
    "author": "",
Confidence
95% confidence
Finding
The dependency version for qrcode uses a caret range (^1.5.1), which allows automatic installation of newer minor and patch releases. This weakens build reproducibility and can unintentionally pull in a compromised or breaking upstream release through the software supply chain.

Unpinned Dependencies

Low
Category
Supply Chain
Content
},
    "dependencies": {
        "qrcode": "^1.5.1",
        "sharp": "^0.32.4"
    },
    "author": "",
    "license": "MIT"
Confidence
98% confidence
Finding
The sharp dependency is specified with a caret range (^0.32.4), so installs may resolve to different releases over time. For a package with native components and prior security advisories, this increases supply-chain risk and makes it harder to verify whether deployed builds are affected by known issues.

Unverifiable Dependency: sharp has 4 known advisory(ies) (GHSA-54xq-cgqr-rpm3 (sharp vulnerability in libwebp dependency CVE-2023-4863); GHSA-f88m-g3jw-g9cj (sharp inherited vulnerabilities in libvips: CVE-2026-33327, CVE-2026-33328, CVE-); CVE-2022-29256 (sharp vulnerable to Command Injection in post-installation over build environmen) +1 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
93% confidence
Finding
The manifest includes sharp without pinning an exact version, while the package has multiple known advisories in some releases. Because the resolved installed version is not fixed here, consumers may unknowingly install an affected version, which is more concerning given sharp's native tooling and historical post-install/build-related security issues.

Missing User Warnings

Low
Confidence
85% confidence
Finding
This code performs a direct file write and atomic rename to the target output path, which can overwrite an existing file in the workspace root. While the script logs the destination path and success, it does not warn the user that an existing file may be replaced or prompt for confirmation before doing so.

Static analysis

No suspicious patterns detected.