T09 · Insecure Skill Coding Practices
- Location
dist/index.cjs:18844- Finding
Configurable Backend URL Can Receive the Broker JWT
- Content
View full analysis
ctrl.abort(), this.options.timeoutMs ...[truncated 4041 chars]- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill largely matches its finance-portfolio purpose, but it handles account JWTs and financial intents while relying on under-enforced and overstated backend and tool-integrity protections.
Install only if you trust MuHaven and can keep the backend URL at the default production value, or run in a runtime that truly enforces the declared network allowlist. Prefer read-only mode if you only need portfolio views, verify the broker daemon and account session are yours, and treat any request to change backend_url, dashboard_url, or broker_endpoint as security-sensitive.
dist/index.cjs:18844Configurable Backend URL Can Receive the Broker JWT
dist/index.cjs:19965Tool-Description Integrity Verification Fails Open When Pin Data Is Missing
Referenced artifact was not completely inspected
entry: dist/index.cjs
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.
function validateAsync() {
const ruleErrs = gen2.let("ruleErrs", null);
gen2.try(() => assignValid((0, codegen_1._)`await `), (e) => gen2.assign(valid, false).if((0, codegen_1._)`${e} instanceof ${it.ValidationError}`, () => gen2.assign(ruleErrs, (0, codegen_1._)`${e}.errors`), () => gen2.throw(e)));
return ruleErrs;
}
function validateSync() {
const validateErrs = (0, codegen_1._)`${validateRef}.errors`;
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.
function validateAsync() {
const ruleErrs = gen2.let("ruleErrs", null);
gen2.try(() => assignValid((0, codegen_1._)`await `), (e) => gen2.assign(valid, false).if((0, codegen_1._)`${e} instanceof ${it.ValidationError}`, () => gen2.assign(ruleErrs, (0, codegen_1._)`${e}.errors`), () => gen2.throw(e)));
return ruleErrs;
}
function validateSync() {
const validateErrs = (0, codegen_1._)`${validateRef}.errors`;
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).
nv.root.schema)
return env;
return void 0;
}
}
});
// ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/refs/data.json
var require_data = __commonJS({
"../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/refs/data.json"(exports$1, module) {
module.exports = {
$id: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#",
description: "Meta-schema for $data reference (JSON AnySchema extension proposal)",
type: "object",
required: ["$data"],
properties: {
$data: {
type: "string",
anyOf: [{ format: "relative-json-pointer" }, { format: "json-pointer" }]
}
},
additionalProperties: false
};
}
});
// ../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js
var require_utils = __commonJS({
"../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js"(exports$1, module) {
init_esm_shims();
var isUUID = Re
YARA rule matched a hack tool or exploit indicator (offensive tools, reconnaissance, privilege escalation, or exploit frameworks).
nv.root.schema)
return env;
return void 0;
}
}
});
// ../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/refs/data.json
var require_data = __commonJS({
"../../node_modules/.pnpm/ajv@8.18.0/node_modules/ajv/dist/refs/data.json"(exports$1, module) {
module.exports = {
$id: "https://raw.githubusercontent.com/ajv-validator/ajv/master/lib/refs/data.json#",
description: "Meta-schema for $data reference (JSON AnySchema extension proposal)",
type: "object",
required: ["$data"],
properties: {
$data: {
type: "string",
anyOf: [{ format: "relative-json-pointer" }, { format: "json-pointer" }]
}
},
additionalProperties: false
};
}
});
// ../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js
var require_utils = __commonJS({
"../../node_modules/.pnpm/fast-uri@3.1.0/node_modules/fast-uri/lib/utils.js"(exports$1, module) {
init_esm_shims();
var isUUID = Re
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"stdio": "manifest_only"
},
"secrets": {
"storage": "os_keychain",
"deny_paste_ui": true,
"references": [
{
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"stdio": "manifest_only"
},
"secrets": {
"storage": "os_keychain",
"deny_paste_ui": true,
"references": [
{
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.
"stdio": "manifest_only"
},
"secrets": {
"storage": "os_keychain",
"deny_paste_ui": true,
"references": [
{
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.
2. **Tool-description hash pinning** — `@muhaven/mcp`'s
`verify-tool-hashes --check` runs at server startup and exits non-zero
on descriptor drift (MCPoison-style attacks fail-closed).
3. **Broker JWT-scope check** — write paths require a JWT scoped to
`mcp.propose.*`. The broker daemon's `signHash` is the only route to
a UserOp signature; the JWT is bound to the user's account.
4. **Three-tier confirmation** — `position.*` + `policy.pause` emit
The statement that the skill 'does not read or write any path on the operator's system' is contradicted by the earlier requirement that it call a local broker over a socket or named-pipe path such as ~/.muhaven/broker.sock. Accessing that IPC endpoint is local filesystem/path interaction, so the documentation presents a stronger no-filesystem-access claim than the behavior described elsewhere in the same file.
The checklist claims that non-MuHaven egress is blocked by the runtime, but earlier comments explicitly state that under host_native fallback in staging the network allowlist is only advisory and not kernel-enforced. This creates a documentation-to-enforcement gap that can mislead reviewers or users into trusting a stronger boundary than actually exists in some deployments.
Skill grants unrestricted tool access without appropriate constraints. An agent with unfettered tool access can perform arbitrary actions including file modification, network requests, and code execution.
* Controls when tools are used:
* - "auto": Model decides whether to use tools (default)
* - "required": Model MUST use at least one tool before completing
* - "none": Model MUST NOT use any tools
*/
mode: _enum(["auto", "required", "none"]).optional()
});
Skill grants unrestricted tool access without appropriate constraints. An agent with unfettered tool access can perform arbitrary actions including file modification, network requests, and code execution.
* Controls when tools are used:
* - "auto": Model decides whether to use tools (default)
* - "required": Model MUST use at least one tool before completing
* - "none": Model MUST NOT use any tools
*/
mode: _enum(["auto", "required", "none"]).optional()
});
The comment states the OpenClaw skill 'never advertises' issuer flows, but the file still embeds the issuer tool descriptors and handler wiring in shared registries at L19268-L19304 and L19885-L19905, only filtering them later in selectOpenClawSubsetRegistry at L20136-L20148. That is an intent/documentation mismatch because the codebase does include and wire those capabilities, even if they are later excluded from the exposed subset.
The statement 'The skill cannot prompt you to paste a token' overstates what this config guarantees. The policy denies paste UI, environment blocks, and clipboard I/O, but that does not inherently prove the skill can never ask a user for a token through other interaction paths; the claim is stronger than the demonstrated control.
The inline comment at L20130-L20144 says the bundled OpenClaw skill 'never advertises' issuer and governance tools. However, the module exports TOOLSET_EXCLUDED at L20176, which contains those excluded tool names, so the code still exposes their identifiers to consumers even if they are not callable. This is a documentation-to-code contradiction about what is exposed by the skill bundle.
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.
"CHANGELOG.md"
],
"devDependencies": {
"@types/node": "^22.0.0",
"rimraf": "^5.0.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.
],
"devDependencies": {
"@types/node": "^22.0.0",
"rimraf": "^5.0.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
The unpinned tsup dependency creates avoidable supply-chain uncertainty because semver ranges allow different versions to be installed over time, including potentially vulnerable or compromised releases. In this case the risk is somewhat elevated because a separate finding notes a known advisory for tsup, and the manifest alone does not let defenders verify whether installs are safe.
"devDependencies": {
"@types/node": "^22.0.0",
"rimraf": "^5.0.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0",
This is a valid supply-chain risk because tsup has a referenced advisory and the manifest's semver range prevents verification of whether an installed version is affected. The danger is primarily to developers and CI systems building the skill rather than end users, but compromised build environments can still taint released artifacts.
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.
"@types/node": "^22.0.0",
"rimraf": "^5.0.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0",
"@muhaven/mcp": "0.1.5"
Dependencies lack version pinning, allowing potential malicious package updates. Consider pinning versions.
"rimraf": "^5.0.0",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0",
"@muhaven/mcp": "0.1.5"
},
Leaving vitest unpinned creates supply-chain ambiguity and makes it difficult to determine whether installed versions are exposed to known advisories. Although vitest is a devDependency, some listed issues involve file read or code execution in developer or CI contexts, so version drift can still create meaningful risk during testing workflows.
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.7.0",
"vitest": "^3.0.0",
"@muhaven/mcp": "0.1.5"
},
"engines": {
This is a true vulnerability signal because vitest has multiple advisories involving arbitrary file read and possible code execution, and the manifest does not pin a version that can be checked against fixed releases. Even though Vitest is development-only, those impacts are serious in local development or CI environments where secrets, repository contents, and signing credentials may be present.
Detected: suspicious.dynamic_code_execution