Back to skill

Security audit

open-health-link

Security checks for vulnerabilities and agentic risk

Overview

This health-data skill mostly matches its stated purpose, but it needs review because account authorization material and health reports are stored or exposed more broadly than necessary.

Review this skill before installing if the machine or workspace is shared, backed up, indexed, or logged. It accesses breo health-account data, stores a reusable local authorization credential, creates temporary QR authorization images, and may expose detailed health report data through agent execution output; prefer a version that uses protected credential storage, redacts raw outputs, and cleans up authorization artifacts.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/poll-auth.js:27
Finding

Bearer Credential Stored Without Restrictive Filesystem Permissions

Content
View full analysis

Vulnerability Details

File Location: scripts/poll-auth.js:27-38, scripts/token-manager.js:105-113, scripts/utils.js:9-19, 41-43
Vulnerability Type: Plaintext sensitive credential storage with inherited default permissions
Risk Level: High

Vulnerable Code

js
async function saveAuthorizedToken(result) {
  const tokenRecord = {
    [AUTH_TOKEN_KEY]: result[AUTH_TOKEN_KEY],
    uid: result.uid || null,
    authType: result.authType ?? null,
    savedAt: new Date().toISOString(),
  };
  await ensureDataDir();
  await writeFile(TOKEN_FILE, JSON.stringify(tokenRecord, null, 2), "utf-8");
  for (const filePath of TOKEN_FALLBACK_FILES) {
    try {
      await unlink(filePath);
    } catch {
      // file may not exist, that's fine
    }
  }
}
js
const tokenRecord = {
  authToken: data.authToken,
  uid: data.uid || null,
  authType: data.authType ?? null,
  savedAt: new Date().toISOString(),
};

await ensureDataDir();
await writeFile(TOKEN_FILE, JSON.stringify(tokenRecord, null, 2), "utf-8");
js
export const OPEN_HEALTH_LINK_DATA_DIR = join(
  SKILL_ROOT_DIR,
  ".open-health-link-data"
);
export const TOKEN_FILE = join(OPEN_HEALTH_LINK_DATA_DIR, "token.json");

export async function ensureDataDir() {
  await mkdir(OPEN_HEALTH_LINK_DATA_DIR, { recursive: true });
}

Technical Analysis

The authorization bearer credential is written as plaintext JSON. Neither directory creation nor file creation specifies a restrictive permission mode. Node.js therefore applies its normal defaults, subject to the process umask. Existing files can also retain previously permissive permissions when overwritten.

Because the credential authorizes access to private scalp-health information, it should be handled as a high-value secret. Storing it beneath the Skill installation directory can be particularly risky when the Skill is installed in a shared ...[truncated 1385 chars]

Remediation
View remediation

Remediation Suggestions

  1. Create the credential directory with mode 0700.
  2. Create or replace the credential file atomically with mode 0600.
  3. Check existing file and directory permissions at startup and repair permissions that are too broad.
  4. Reject symbolic links and unexpected file types before reading or replacing the credential file.
  5. Prefer an operating-system credential manager or encrypted secret store instead of plaintext JSON.
  6. Apply the same validation to legacy fallback credential files before reading them.
  7. Document credential retention, revocation, and deletion behavior.
  8. Add automated tests that fail when the stored credential is readable by group or other users.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/token-manager.js:122
Finding

Bearer Credential Can Be Exposed Through Standard Output, Command Arguments, and Request URLs

Content
View full analysis

Vulnerability Details

File Location: scripts/token-manager.js:122-131, scripts/fetch-report-list.js:29-43, 201-209
Vulnerability Type: Sensitive credential exposure through observable process and logging channels
Risk Level: Medium

Vulnerable Code

js
async function cmdGet() {
  const token = await loadToken();
  if (!isTokenValid(token)) {
    jsonOutput({ valid: false, authToken: null });
    return;
  }
  jsonOutput({ valid: true, authToken: token.authToken });
}
js
function parseArgs(argv) {
  const args = argv.slice(2);
  let authToken = null;
  let day = null;

  for (let i = 0; i < args.length; i += 1) {
    const arg = args[i];
    if (!arg) {
      continue;
    }
    if (!arg.startsWith("--") && !authToken) {
      authToken = arg;
      continue;
    }
js
async function fetchReportList({ authToken, day }) {
  const baseUrl = getDataApiBaseUrl();
  const path = `/auth/dt/${encodeURIComponent(authToken)}/list`;
  const url = new URL(`${baseUrl}${path}`);
  if (day !== null) {
    url.searchParams.set("day", String(day));
  }

  const resp = await fetch(url, {
    method: "GET",
    signal: AbortSignal.timeout(12000),
  });

Technical Analysis

The credential manager provides a command that emits the complete bearer credential to standard output. The report utility separately accepts the credential as a positional command-line argument. Command-line arguments may be visible in process listings, shell history, orchestration telemetry, crash reports, and command audit logs.

The report endpoint also embeds the credential in the URL path. HTTPS protects the URL in transit from passive network observers, but the path can still be recorded by the local runtime, reverse proxies, API gateways, observability systems, server access logs, and error reports.

The Skill normally loads the credential directly from l ...[truncated 1158 chars]

Remediation
View remediation

Remediation Suggestions

  1. Remove the get command or replace it with a non-sensitive validity/status response.
  2. Remove positional credential support from fetch-report-list.js.
  3. If an explicit credential input is operationally necessary, accept it through protected standard input or an authenticated inter-process channel rather than command arguments.
  4. Change the backend contract to carry the credential in an Authorization header instead of the URL path.
  5. Until the backend is redesigned, configure clients, gateways, and servers to redact the sensitive path component.
  6. Never include the complete request URL in errors, traces, analytics, or access logs.
  7. Rotate or revoke credentials that may already have appeared in historical logs.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/generate-qrcode.js:55
Finding

Secret-Bearing Authorization QR Image Is Persisted Without Secure Cleanup

Content
View full analysis

Vulnerability Details

File Location: scripts/generate-qrcode.js:55-75, scripts/utils.js:35-39
Vulnerability Type: Unsafe temporary file containing authorization material
Risk Level: Medium

Vulnerable Code

js
function buildAuthUrl(code, secret) {
  const params = new URLSearchParams({ code, secret });
  return `${BREO_QR_BASE_URL}?${params.toString()}`;
}

async function generateQRFile(authUrl, code) {
  const qrDataUrl = await QRCode.toDataURL(authUrl, {
    type: "image/png",
    width: QR_IMAGE_SIZE,
    margin: 2,
    color: { dark: "#000000", light: "#FFFFFF" },
  });

  const base64Data = qrDataUrl.replace(/^data:image\/png;base64,/, "");
  const buffer = Buffer.from(base64Data, "base64");
  const filename = `breo-qr-${code.slice(0, 8)}.png`;

  mkdirSync(WORKSPACE_DIR, { recursive: true });
  const qrPath = join(WORKSPACE_DIR, filename);
  writeFileSync(qrPath, buffer);
  return { qrPath };
}
js
export function getWorkspaceDir() {
  const raw = String(process.env.OPENCLAW_WORKSPACE || "").trim();
  return raw || resolve(process.cwd());
}

Technical Analysis

The generated QR image encodes both an authorization code and a secret. It is therefore a temporary capability-bearing artifact, not an ordinary presentation image.

The file is written to an environment-selected workspace or the current working directory. No private temporary directory, explicit file mode, unpredictable random filename, exclusive-create protection, or cleanup operation is used. The filename also includes a predictable authorization-code prefix. The file remains present after authorization succeeds, fails, or expires.

Authorization QR generation and transmission to the user are necessary for the declared functionality. Persistent storage in a general workspace is not necessary once the image has been delivered and the polling flow has completed.

Attack Path

  1. Th ...[truncated 1117 chars]
Remediation
View remediation

Remediation Suggestions

  1. Create QR images inside a dedicated private temporary directory with mode 0700.
  2. Create each image with mode 0600, exclusive-create semantics, and a cryptographically random filename.
  3. Validate that the destination is not a symbolic link and remains within the intended private directory.
  4. Delete the image immediately after the authorization polling process succeeds, fails, expires, or is cancelled.
  5. Install cleanup handling for normal exits, exceptions, and termination signals.
  6. On startup, remove stale authorization images older than the maximum validity period.
  7. Avoid placing capability-bearing QR files in shared or synchronized workspaces.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch-report-list.js:184
Finding

Complete Health Reports and Duplicate Raw Responses Are Emitted to Standard Output

Content
View full analysis

Vulnerability Details

File Location: scripts/fetch-report-list.js:184-198, 226-228
Vulnerability Type: Excessive disclosure of sensitive health information
Risk Level: Medium

Vulnerable Code

js
function buildResult(json, day) {
  const rawData = json?.data ?? null;
  const reports = normalizeReportList(rawData).map(withSchemeName);
  const reportSchemeMap = reports
    .map(toReportSchemeEntry)
    .filter((item) => !!item.schemeName);
  const latestWithScheme = getLatestReportWithScheme(reports);

  return {
    day,
    total: reports.length,
    latestSchemeName: latestWithScheme?.schemeName || null,
    reportSchemeMap,
    reports,
    rawData,
  };
}
js
jsonOutput(buildResult(json, day));

Technical Analysis

The script outputs every normalized report and also includes the backend response again as rawData. This duplicates potentially sensitive health information and forwards every backend-provided field without an allowlist.

According to the project schema, report data can include scalp-health scores, dimension assessments, device information, analysis text, report identifiers, timestamps, thumbnail URLs, and full detection-image URLs. Standard output is commonly captured by Agent execution logs, orchestration systems, terminal history buffers, debugging tools, or transcript retention systems.

The declared workflow requires enough structured information to answer the user's specific request, but it does not require indiscriminately returning both complete reports and the raw response for every query. This violates data-minimization principles.

Attack Path

  1. An authorized user asks the Skill to retrieve a scalp report.
  2. The backend returns the user's health-report data.
  3. The script places the normalized reports and complete rawData response into standard output.
  4. The Agent runtime or surrounding infrastructure records that output ...[truncated 684 chars]
Remediation
View remediation

Remediation Suggestions

  1. Remove rawData from standard output.
  2. Do not return both raw and normalized copies of the same report.
  3. Define a strict allowlist containing only fields needed for the requested summary, trend, or plan lookup.
  4. Redact account identifiers, report identifiers, image URLs, and other fields unless the current user request specifically requires them.
  5. Separate summary and detail operations so the default query retrieves and returns the minimum necessary data.
  6. Configure the Agent runtime not to persist sensitive script output in general-purpose logs.
  7. Apply retention limits and access controls to unavoidable health-data logs.
  8. Add tests that reject unexpected backend fields from serialized output.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

The declared description is for a narrowly scoped health-device data assistant handling Open Health Link and breo Scalp5 account/data operations. The supplied code does not implement any of those behaviors. Instead, it provides a generic file-loading helper that reads text from an arbitrary filesystem path. This is a materially different primary purpose and accesses a resource type (local files) not suggested by the description. Therefore, the description does not accurately represent the code chunk.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill instructs the agent to use networked Node scripts and manage local authorization state, but it does not declare any explicit tool scope such as allowed tools or permissions. This creates a least-privilege violation: the runtime may permit broader filesystem, environment, or network access than users and reviewers expect, increasing blast radius if the skill or its scripts are modified or abused.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The skill handles sensitive health data and account authorization with remote services, but the user-facing description does not clearly warn that authorization artifacts and health-related query data will be transmitted to third-party endpoints. This weakens informed consent and can cause users to disclose or authorize sensitive personal data without understanding where it will go.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The guide states that authorization tokens are stored locally in predictable paths, but it does not clearly warn users that these tokens are sensitive bearer credentials requiring filesystem protection. If other local users, malware, backups, or synced folders can access these files, an attacker may reuse the token to access linked health-account data without re-authentication.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The API reference exposes an authorization flow that returns a signing secret and later uses authToken values, but provides no handling guidance marking these as sensitive credentials. In an agent setting, lack of explicit privacy and secret-handling rules increases the risk that logs, chat responses, debug output, or QR payload construction could leak reusable tokens or secrets.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The manifest limits the skill to account bind/unbind, report lookup, and care-plan lookup for Open Health Link / breo Scalp5 data. This reference adds a separate capability to pull plan details from a remote CSV knowledge base, which is not described as part of the Open Health Link backend and is an additional external data source rather than an obvious requirement of the stated scope.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

This code emits multiple natural-language strings only in Chinese for invalid input, authentication failures, network errors, and general failures. Because the file does not provide user opt-in, fallback language handling, or documentation that the skill is intentionally Chinese-only, it violates the language/locale policy for natural-language content.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The code emits user-facing error text in Chinese at L42, L50, and later in the main error handler, but there is no indication that the skill is intentionally limited to Chinese-speaking users or that users can opt into another language. This is a natural-language locale policy concern because it imposes a specific language regardless of user preference.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

When --save is used, the script writes an authorization token to disk and deletes fallback token files, which affects user credentials and local state. Although the behavior is inferable from the code, there is no user-facing log, prompt, or warning in this file disclosing that sensitive auth data will be persisted and old token files removed.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The get command returns the raw stored authToken to stdout with no additional access control, masking, or confirmation step. In a local CLI context this can expose bearer credentials to any caller, logs, wrapper processes, shell history workflows, or other components in the agent stack that invoke the script, enabling unauthorized use of the linked health account.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
85% confidence
Finding

The unbind flow tells the user the account is unbound, but it does not explicitly explain that locally stored authorization state will be deleted and that subsequent access to linked health data through the skill will stop. This is primarily a transparency and consent issue that can surprise users or cause accidental loss of expected access.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

SQP-3 applies to all file types and includes language or locale policy violations. The entire setup guide forces Chinese-language interaction and trigger phrases, with no indication that users may choose another language or that the locale limitation is intentional and justified.

Content

No source excerpt is available for this finding.

Vague Triggers

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

This markdown file documents activation phrases, so SQP-1 applies. The phrase "打开 Open Health Link" is a generic open/start command that could overlap with ordinary requests to open the skill or related resources, and the guide does not provide exclusion conditions or boundaries for when it should or should not activate.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
62% confidence
Finding

The file states that the current skill always uses the production environment and does not support switching environments. Because the endpoints are fixed to a China-hosted production service and the document provides no user opt-in or justification for this regional/locale constraint, this may conflict with language/locale policy expectations.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

该 markdown 文件通篇以中文编写,且未说明这是面向特定中文用户群或区域的限定文档,也未提供多语言选择。按照语言/locale 政策,若技能内容强制特定语言而没有用户选择或合理说明,属于自然语言层面的策略问题。

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
93% confidence
Finding

The dependency version for qrcode is specified with a caret range (^1.5.4), which allows automatic installation of newer compatible releases. This creates a supply-chain risk because future upstream versions could introduce a vulnerable or compromised package version without an explicit review or lock to an exact release.

Content

Scanner excerpt · scripts/package.json (reported line 11)May include surrounding context.

json
"node": ">=18.0.0"
  },
  "dependencies": {
    "qrcode": "^1.5.4"
  }
}

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
94% confidence
Finding

This code emits multiple hard-coded Chinese-only error and usage messages, such as the validation message at L088 and the runtime errors through L392. Because the file provides no opt-in, locale selection, or documented region-specific justification, it violates the language/locale policy criterion for natural-language content.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

Several user-facing error messages are emitted only in Chinese, which enforces a specific language without any visible opt-in or locale selection. This can violate language/locale policy where skills should not force a language absent documented justification or user choice.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.