Back to skill

Security audit

Openrouter Rankings Screenshot

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its advertised OpenRouter-to-Feishu report purpose, but its browser capture script is under-restricted and can be pointed at arbitrary sites while running Chromium without sandboxing.

Review before installing. This skill is suitable only in an environment where you are comfortable with automated Feishu document creation and daily reporting. The publisher should remove or allowlist the URL argument, avoid running Chromium without sandboxing where possible, restrict network egress, validate manifest file paths before upload, and update the flagged dependencies.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/capture-rankings.mjs:332
Finding
Arbitrary URL Navigation in an Unsandboxed Chromium Process<![CDATA[ ## Vulnerability Details **File Location**: `scripts/capture-rankings.mjs`, lines 332–364 **Vulnerability Type**: Unrestricted browser navigation and disabled process sandbox **Risk Level**: High ### Vulnerable Code ```js const url = process.argv[2] || DEFAULT_URL; ``` ```js const browser = await puppeteer.launch({ executablePath: CHROMIUM, headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-gpu'], }); ``` ```js console.log(`🌐 Loading ${url} ...`); await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 120000 }); ``` ### Technical Analysis The second command-line argument directly controls the URL passed to `page.goto()` without validating its protocol, hostname, port, resolved address, path, or redirects. Although the Skill is intended to capture `https://openrouter.ai/rankings`, the implementation permits Chromium to navigate to an arbitrary attacker-selected destination. This creates a server-side request forgery-like browser primitive. Depending on Chromium's supported protocols and environmental controls, the browser may be able to access: - Loopback services such as `127.0.0.1` or `[::1]` - Private network services - Link-local and cloud metadata endpoints - Attacker-controlled web pages - Services available only from the host's trusted network The risk is amplified because Chromium is launched with both `--no-sandbox` and `--disable-setuid-sandbox`. These options remove important browser isolation boundaries. If an attacker-controlled page exploits a Chromium vulnerability, the resulting code may execute with the privileges of the Node.js process rather than being contained by the Chromium sandbox. Redirects are also not restricted. Consequently, validating only the original URL would be insufficient unless every redirect destination is checked. ### Attack Path 1. An attacker gains influence over how `capture-rankings.mjs` is invoked, such as through an Agent-suppli ...[truncated 1744 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove support for a command-line URL if customization is unnecessary: ```js const url = DEFAULT_URL; ``` 2. If customization is required, enforce an exact allowlist: ```js function validateTarget(input) { const target = new URL(input); if (target.protocol !== 'https:') { throw new Error('Only HTTPS URLs are permitted'); } if (target.hostname !== 'openrouter.ai') { throw new Error('Only openrouter.ai is permitted'); } if (target.pathname !== '/rankings') { throw new Error('Only the rankings page is permitted'); } if (target.username || target.password || target.port) { throw new Error('Credentials and custom ports are not permitted'); } return target.href; } ``` 3. Resolve the destination hostname and reject loopback, private, link-local, multicast, and reserved IPv4 and IPv6 ranges. 4. Intercept browser requests and restrict navigation and subresource requests to explicitly approved origins. 5. Validate every redirect destination rather than checking only the initial URL. 6. Remove `--no-sandbox` and `--disable-setuid-sandbox`. Run Chromium as a dedicated non-root user with the browser sandbox enabled. 7. Apply outbound firewall or container network restrictions so the process cannot reach loopback administration services, private networks, or cloud metadata endpoints unless explicitly required. 8. Keep Chromium patched and use a browser version compatible with the installed `puppeteer-core` release. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/validate-screenshots.mjs:10
Finding
Manifest-Controlled File Paths Can Reach the Feishu Upload Workflow<![CDATA[ ## Vulnerability Details **File Location**: `scripts/validate-screenshots.mjs`, lines 10–34 **Vulnerability Type**: Insufficient validation of externally supplied file paths **Risk Level**: Low ### Vulnerable Code ```js const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); const minBytes = Number(process.env.MIN_SCREENSHOT_BYTES || 28000); let ok = true; function check(label, filePath, extra = '') { if (!filePath) return; const stat = fs.statSync(filePath); if (stat.size < minBytes) { console.error(`FAIL ${label}: ${stat.size} bytes (< ${minBytes})`); ok = false; } else { console.log(`OK ${label}: ${stat.size} bytes${extra}`); } } for (const section of manifest.sections || []) { if (!section.file) continue; check(section.heading, section.file, section.barCount != null ? `, bars=${section.barCount}` : ''); } for (const cat of manifest.categoryScenarios || []) { if (!cat.file) continue; const top = cat.rankings?.[0]?.model ? `, top=${cat.rankings[0].model}` : ''; check(`Categories/${cat.scenario}`, cat.file, top); ``` ### Technical Analysis The validator accepts a manifest path from the command line, parses file paths from that manifest, and passes those paths directly to `fs.statSync()`. It does not establish that a referenced file: - Is located under the expected rankings output directory - Is a regular file - Is not a symbolic link - Is actually a PNG image - Has an expected generated filename - Was created by the current capture operation The only substantive file validation is a minimum-size check. The Skill instructions subsequently direct the Agent to pass absolute paths from `manifest.json` to `feishu_doc_media`. Therefore, a tampered or attacker-supplied manifest could reference another locally readable image and cause that file to be accepted by validation and uploaded during the documented workflow. This issue requires the attacker to control or modify the manifest and is limited by the ...[truncated 1874 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Derive the trusted output root independently instead of trusting `manifest.outputDir`. 2. Canonicalize both the trusted root and each candidate path: ```js const trustedRoot = fs.realpathSync(expectedOutputDirectory); const candidate = fs.realpathSync(filePath); const relative = path.relative(trustedRoot, candidate); if (relative.startsWith('..') || path.isAbsolute(relative)) { throw new Error('Screenshot path escapes the trusted output directory'); } ``` 3. Use `fs.lstatSync()` before resolving the path and reject symbolic links: ```js const linkStat = fs.lstatSync(filePath); if (linkStat.isSymbolicLink()) { throw new Error('Symbolic links are not permitted'); } ``` 4. Require a regular file: ```js const stat = fs.statSync(candidate); if (!stat.isFile()) { throw new Error('Screenshot path is not a regular file'); } ``` 5. Verify the PNG signature rather than relying on extension or size alone: ```js const signature = fs.readFileSync(candidate, { encoding: null, flag: 'r' }).subarray(0, 8); const expected = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); if (!signature.equals(expected)) { throw new Error('File is not a valid PNG'); } ``` 6. Enforce generated filename patterns and reject unexpected manifest entries. 7. Bind the manifest to the current capture run, for example by generating it and consuming it within the same trusted process or by recording cryptographic hashes for every screenshot. 8. Before invoking `feishu_doc_media`, repeat canonical-path, regular-file, and image-signature validation at the upload boundary. ]]>
Vulnerability Patterns
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (7)

Known Vulnerable Dependency: extract-zip==2.0.1 — 2 advisory(ies): CVE-2026-19693 (extract-zip allows arbitrary file writes through symlink archive entries); CVE-2026-56876 (extract-zip unvalidated symlink path traversal)

High
Category
Supply Chain
Confidence
90% confidence
Finding
extract-zip 2.0.1 is a real supply-chain risk because archive extraction bugs involving symlink handling can let a crafted archive write files outside the intended destination. In this dependency tree it is brought in by Puppeteer browser-management tooling, so exploitation would require the skill to process or download attacker-influenced archives, but if that occurs it can lead to arbitrary file overwrite and possible code execution or persistence.

Known Vulnerable Dependency: ip-address==10.2.0 — 3 advisory(ies): CVE-2026-54272 (ip-address: misclassification of IPv4-mapped/NAT64 IPv6 addresses can bypass SSR); CVE-2026-69198 (ip-address: a CIDR suffix on the parsed address suppresses special-use classific); CVE-2026-69192 (ip-address: Address4 decodes leading-zero octets as decimal while resolvers deco)

High
Category
Supply Chain
Confidence
82% confidence
Finding
ip-address 10.2.0 has reported address-classification flaws that can mis-handle IPv4-mapped, NAT64, or CIDR-suffixed inputs, which matters when software relies on it to decide whether a destination is private, loopback, or otherwise restricted. In this lockfile it is only a transitive dependency of proxy-related packages, and there is no direct evidence in this file that the skill performs security-sensitive SSRF filtering with it, so the issue is real but contextually less exposed here.

Known Vulnerable Dependency: ws==8.20.1 — 1 advisory(ies): CVE-2026-48779 (ws: Memory exhaustion DoS from tiny fragments and data chunks)

High
Category
Supply Chain
Confidence
88% confidence
Finding
ws 8.20.1 is affected by a memory-exhaustion denial-of-service issue where an attacker can send fragmented frames or tiny chunks that cause excessive resource consumption. Because Puppeteer uses WebSocket transport to speak to the browser, this is a real dependency risk, but in this skill the exposure is likely limited to local or trusted DevTools/browser communication unless an attacker can connect to or influence that WebSocket endpoint.

Lp3

Medium
Category
MCP Least Privilege
Confidence
84% confidence
Finding
The skill instructs the agent to run local commands and access local files, but it does not declare any explicit tool or permission scope. That mismatch can cause the runtime to grant broader-than-intended capabilities by default, reducing auditability and increasing the chance of unauthorized environment or filesystem access if the skill is reused or modified.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The script uses `const url = process.argv[2] || DEFAULT_URL;` and then passes that value directly to `page.goto(...)`, so an operator or upstream agent can make it browse to any arbitrary site rather than the intended `openrouter.ai/rankings` page. In an agent/automation context this expands the skill from a narrowly scoped screenshot tool into a general-purpose headless browser fetcher, which can be abused for SSRF-style access to internal HTTP endpoints, interaction with sensitive intranet pages, or retrieval of untrusted content under the agent's network identity.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The script hardcodes all generated headings and summary text in Chinese, including the markdown report and chat summary. There is no option for language selection or any documented justification that this skill is intended only for a Chinese-language audience, which creates a natural-language locale policy concern.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "ISC",
  "description": "",
  "dependencies": {
    "puppeteer-core": "^23.11.1"
  }
}
Confidence
40% confidence
Finding
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.

Static analysis

No suspicious patterns detected.