Back to skill

Security audit

Heimdall

Security checks across malware telemetry and agentic risk

Overview

This appears to be a legitimate testing tool, but it can use credentials and perform network or file side effects with controls that are too broad for untrusted plans.

Install only if you are comfortable treating Heimdall plans as trusted code-like test fixtures. Before running a plan, inspect setup/teardown, load steps, eval steps, baseline paths, storageState use, and any ${env.*} references; use least-privilege credentials and keep evidence directories private because they can contain sessions or secrets.

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
src/runner.ts:268
Finding
Plan-Level Hooks Bypass Risk Authorization Controls<![CDATA[ ## Vulnerability Details **File Location**: `src/runner.ts:268-290` and `src/schema.ts:579-589` **Vulnerability Type**: Missing authorization enforcement for plan-level setup and teardown hooks **Risk Level**: High ### Complete Code Snippet ```ts if (RISKY.has(tc.risk) && !opts.allowRisk) { return { tc, driver, runnable: false, blockReason: `risk=${tc.risk}: blocked unless --allow-risk is set (and confirmed by a human).`, }; } return { tc, driver, runnable: true }; ``` ```ts // By design, plan hooks are TRUSTED, author-controlled fixtures and run UNGATED: // unlike cases, they are not subject to the RISKY/--allow-risk gate (a hook has // no per-step risk field). Authors must keep destructive ops out of plan // setup/teardown unless they intend them to run unconditionally. const hadRunnable = runnable.length > 0; if (plan.setup && plan.setup.length > 0 && hadRunnable) { log.info(c.cyan("▶ plan setup") + c.dim(` — ${plan.setup.length} step(s)`)); const setupError = await runPlanHook(plan.setup, ctx, "plan setup"); if (setupError) { const reason = redactString(`plan setup failed: ${setupError}`); log.err(reason); for (const p of runnable) { results.push(blocked(p.tc, p.driver, reason)); log.warn(`${p.tc.id}: ${reason}`); } runnable = []; } } ``` The schema explicitly documents the same behavior: ```ts "steps run ONCE before any case (in a throwaway context) to seed shared state; if any fails, every runnable case is marked blocked. TRUSTED, author-controlled fixtures: plan hooks run UNGATED — they are NOT subject to the per-case risk/--allow-risk gate, so keep destructive operations out of them unless you intend them to run unconditionally" ``` ### Technical Analysis The authorization control applies only to individual cases whose `risk` value is `destructive`, `paid`, or `prod`. Plan-level `setup` and `teardown` hooks support the same powerful step interpreter, including HTTP requests, br ...[truncated 2047 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add explicit risk metadata to plan-level `setup` and `teardown` hooks. 2. Apply the same `RISKY` and `allowRisk` checks to hooks before creating a browser or issuing any request. 3. Prefer requiring a separate option such as `--allow-risky-hooks`, so case authorization cannot silently authorize unconditional fixtures. 4. Reject network, `eval`, `load`, and other side-effecting hook steps unless the hook is explicitly authorized. 5. In the MCP interface, expose risky-hook authorization as a distinct, clearly described parameter and rely on the MCP host’s approval mechanism. 6. Consider eliminating plan-level side-effecting hooks in favor of normal cases so every operation has a risk label, result, timeout, and evidence trail. 7. Add regression tests proving that destructive operations in both setup and teardown remain blocked without authorization. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/execute.ts:313
Finding
Unrestricted Screenshot Baseline Path Allows Arbitrary File Creation<![CDATA[ ## Vulnerability Details **File Location**: `src/schema.ts:458-470` and `src/execute.ts:313-321` **Vulnerability Type**: Path traversal and unrestricted filesystem write **Risk Level**: High ### Complete Code Snippet The schema accepts any string as a baseline path: ```ts z .object({ assert: z.literal("screenshotMatches"), baseline: z.string().describe("path to the baseline PNG to compare the capture against"), maxDiffRatio: z .number() .min(0) .max(1) .default(0.01) .describe("maximum fraction of differing pixels tolerated (0..1)"), selector: z.string().optional().describe("clip the capture to this element instead of the full page"), }) .strict() .describe("pixel-compare a screenshot against a stored baseline (pixelmatch); fail above maxDiffRatio") ``` The path is then used directly for filesystem access: ```ts try { const current = await obs.screenshot(o.selector); if (!existsSync(o.baseline)) { await writeFile(o.baseline, current); // first run: the current capture becomes the baseline continue; } const baseline = await readFile(o.baseline); const { diffBuffer } = comparePng(baseline, current); const diffPath = join(caseDir, `screenshot-diff-${i + 1}.png`); await writeFile(diffPath, diffBuffer); screenshots.push(diffPath); } catch (e) { log.debug(`screenshot baseline/diff for oracle ${i + 1} failed: ${String(e)}`); } ``` ### Technical Analysis The `baseline` field is fully controlled by the plan and is neither normalized nor restricted to an approved baseline directory. Absolute paths and relative traversal sequences such as `../../...` are accepted. When the target does not exist, `persistVisualBaselines` writes the captured PNG directly to that path. When it exists, the function reads it and attempts to parse it as a PNG. Although parsing constrains the usefulness of arbitrary reads, the behavior can still reveal whether a path exists or is readable through ob ...[truncated 1607 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Introduce an explicit trusted baseline root directory. 2. Reject absolute paths in plan-provided `baseline` values. 3. Resolve the requested path against the baseline root and verify that the normalized result remains within that root: ```ts const root = resolve(configuredBaselineRoot); const target = resolve(root, o.baseline); if (target !== root && !target.startsWith(root + sep)) { throw new Error("baseline path escapes the configured baseline root"); } ``` 4. Resolve and validate symbolic links where existing path components may redirect access outside the root. 5. Do not create missing baselines during an ordinary test run. Require an explicit option such as `--update-baselines`. 6. Use exclusive file creation where appropriate to avoid overwriting files created concurrently. 7. Apply restrictive file permissions and verify that the destination has an expected image extension. 8. Add tests for absolute paths, `..` traversal, symbolic-link escapes, and baseline writes attempted without update authorization. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/schema.ts:165
Finding
Unbounded Load Parameters Enable Request Amplification and Resource Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `src/schema.ts:165-181` and `src/execute.ts:552-578` **Vulnerability Type**: Unbounded resource consumption and network request amplification **Risk Level**: High ### Complete Code Snippet The schema requires positive values but imposes no upper limits: ```ts z .object({ action: z.literal("load"), url: z.string(), method: z.string().optional(), headers: z.record(z.string()).optional(), body: z.string().optional(), times: z.number().int().positive().describe("total number of request iterations to issue"), concurrency: z .number() .int() .positive() .optional() .describe("max in-flight requests; defaults to min(times, 10)"), as: z.string().optional().describe("name to store the load aggregate under for oracles"), }) .strict() ``` The values directly control request volume and worker count: ```ts case "load": { const method = step.method ?? "GET"; const concurrency = step.concurrency ?? Math.min(step.times, 10); const rc = page.context().request; const url = resolveUrl(applyVars(step.url, store.vars), baseUrl); const headers = applyVarsHeaders(step.headers, store.vars); const data = step.body === undefined ? undefined : applyVars(step.body, store.vars); const samples = await runConcurrent<LoadSample>(step.times, concurrency, async () => { const t0 = Date.now(); try { const r = await rc.fetch(url, { method, headers, data }); const sample = { status: r.status(), durationMs: Date.now() - t0, error: !r.ok() }; await r.dispose(); return sample; } catch { return { status: 0, durationMs: Date.now() - t0, error: true }; } }); const stats = summarizeLoad(samples); const status = modalStatus(samples); const bodyText = formatLoadStats(stats); ``` ### Technical Analysis Both `times` and `concurrency` accept any positive integer. The values are passed directly to the concurrent execu ...[truncated 2071 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add conservative schema limits to both fields, for example: ```ts times: z.number().int().min(1).max(1000), concurrency: z.number().int().min(1).max(20).optional() ``` Defaults should be substantially lower for agent-triggered or MCP execution. 2. Treat every `load` step as inherently risky regardless of the author-supplied case label. 3. Require an explicit authorization option dedicated to load testing, such as `--allow-load`. 4. Enforce a total request budget across nested `race` steps, retries, cases, and plan hooks rather than validating each step independently. 5. Add destination restrictions or an allowlist for MCP and other untrusted-plan contexts. 6. Reject load testing against loopback, link-local, metadata-service, and private-network addresses unless explicitly authorized. 7. Implement cancellation with `AbortController` or equivalent driver teardown so wall-clock timeouts stop outstanding requests. 8. Add process-level safeguards, including memory, CPU, connection, and execution-time limits. 9. Add tests covering maximum values, aggregate nested load volume, retries, timeout cancellation, and unauthorized load execution. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Description-Behavior Mismatch

Low
Confidence
95% confidence
Finding
The example plan labels the login-flow case as "read-only" even though submitting credentials and establishing a session typically creates or mutates server-side authentication/session state. This mismatch can mislead downstream agents or operators into treating the case as non-mutating, which weakens safety gating and may cause the test to run in contexts where state-changing actions are not allowed.

Context-Inappropriate Capability

Medium
Confidence
93% confidence
Finding
The `${env.NAME}` templating path lets an untrusted plan read arbitrary process environment variables and splice them into URLs, headers, or bodies at runtime. In this skill, those values can then be sent to remote web apps/APIs under test and may also persist in secret-bearing artifacts such as HAR/trace files, so this becomes a real secret-exfiltration capability beyond ordinary browser test execution.

Context-Inappropriate Capability

Medium
Confidence
89% confidence
Finding
`persistVisualBaselines` reads and writes `o.baseline` directly with `existsSync`, `readFile`, and `writeFile` without constraining the path to the case evidence directory or another safe root. A crafted plan can therefore cause file creation or overwrite at arbitrary filesystem locations writable by the process, which exceeds the expected scope of storing test artifacts.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
The code registers secret values for report redaction but the warning about HAR/trace leakage exists only in comments, not as an enforced or user-facing control at the moment of secret access. Because this skill is specifically designed to drive live browser/API interactions, silently injecting env secrets into requests can surprise operators and leak credentials to tested services or captured evidence.

VirusTotal

VirusTotal findings are pending for this skill version.

View on VirusTotal

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/execute.ts:57