Back to skill

Security audit

Legion Loan Outreach Insight

Security checks for vulnerabilities and agentic risk

Overview

The skill is meant to analyze ASR call transcripts, but it handles bearer tokens, tenant identifiers, and transcript records with too little scoping or protection.

Review carefully before installing. This should not be used with real customer or employee recordings until endpoint allowlisting, HTTPS, tenant binding, validated time ranges, transcript/field minimization, and bounded/redacted error logging are added. The scripts also currently contain a JavaScript syntax error in parse-time-range.mjs, so the skill does not run as packaged.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch-asr-recordings.mjs:11
Finding
Bearer Token Forwarding to Arbitrary Plaintext Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-asr-recordings.mjs:11-18`, `scripts/fetch-asr-recordings.mjs:57-67`, and `scripts/fetch-asr-recordings.mjs:103-124` **Vulnerability Type**: Arbitrary credential destination and insecure transport **Risk Level**: High ### Vulnerable Code ```js function isHttpBaseUrl(value) { return /^https?:\/\//i.test(String(value ?? "").trim()); } function normalizeBaseUrl(value) { if (!isHttpBaseUrl(value)) { return DEFAULT_BASE_URL; } return String(value).trim().replace(/\/+$/, ""); } ``` ```js function resolveBaseUrl(argvBase) { if (isHttpBaseUrl(argvBase)) { return normalizeBaseUrl(argvBase); } const fromEnv = process.env.LEGION_HARDWARE_BASE_URL?.trim(); if (isHttpBaseUrl(fromEnv)) { return normalizeBaseUrl(fromEnv); } return DEFAULT_BASE_URL; } ``` ```js async function fetchRecordings(root, { userId, orgId, startTime, endTime, token }) { const headers = { Authorization: `Bearer ${token}`, Accept: "application/json", }; const getUrl = new URL(`${root}/api/recordings/asr-completed`); getUrl.searchParams.set("userId", userId); getUrl.searchParams.set("orgId", orgId); getUrl.searchParams.set("startTime", startTime); getUrl.searchParams.set("endTime", endTime); let res = await fetch(getUrl, { method: "GET", headers }); if (res.status !== 405) { return { res, method: "GET" }; } res = await fetch(`${root}/api/recordings/asr-completed`, { method: "POST", headers: { ...headers, "Content-Type": "application/json" }, body: JSON.stringify({ userId, orgId, startTime, endTime }), }); return { res, method: "POST" }; } ``` ### Technical Analysis The base URL may be supplied through a command-line argument or `LEGION_HARDWARE_BASE_URL`. Validation only checks whether the value begins with `http://` or `https://`; it does not enforce TLS, validate the destination against an allowlist, or bind the credential to the intended Legion H ...[truncated 1590 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require `https://` for every credential-bearing request. - Permit only explicitly configured Legion Hardware hostnames and ports. - Parse the URL and validate `protocol`, `hostname`, and `port` independently; do not rely on a prefix regular expression. - Reject embedded credentials and unexpected URL components. - Disable redirects for authenticated requests or revalidate the origin before following each redirect. - Do not forward an authorization header across origins. - Prefer a fixed service endpoint supplied by trusted deployment configuration rather than command-line input. - If an internal service cannot support HTTPS, place it behind authenticated TLS or use mTLS over a tightly controlled network. - Rotate any token that may previously have been transmitted over an untrusted or plaintext connection. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/fetch-asr-recordings.mjs:69
Finding
Caller-Controlled Tenant Identifiers Are Not Bound to Authenticated Token Claims<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-asr-recordings.mjs:69-86` and `scripts/fetch-asr-recordings.mjs:130-166` **Vulnerability Type**: Missing tenant identity and object-level authorization enforcement **Risk Level**: High ### Vulnerable Code ```js function resolveInput(argv, body) { const [, rawArgvBase, argUserId, argOrgId, argToken] = argv; const baseUrl = resolveBaseUrl(rawArgvBase); if (argUserId && argOrgId) { return { baseUrl, body: { userId: String(argUserId).trim(), orgId: String(argOrgId).trim(), userMessage: process.env.LEGION_USER_MESSAGE?.trim() || "", skipClarification: process.env.LEGION_SKIP_CLARIFICATION === "1", }, token: extractBearerToken(argToken) || resolveToken(), }; } return { baseUrl, body: body && typeof body === "object" ? body : {}, token: resolveToken(), }; } ``` ```js const stdinBody = await readRequestBody(); const { baseUrl, body, token } = resolveInput(process.argv, stdinBody); const userId = body.userId != null ? String(body.userId).trim() : null; const orgId = body.orgId != null ? String(body.orgId).trim() : null; if (!userId || !orgId) { console.error( JSON.stringify({ ok: false, error: "userId 与 orgId 必须从请求体 JSON 提供(字段 userId、orgId)", }), ); process.exit(1); } if (!token) { console.error( JSON.stringify({ ok: false, error: "token 必须从 Authorization(Bearer)或 LEGION_AUTH_TOKEN 提供,勿使用 body.token", }), ); process.exit(1); } ``` ```js const { res, method } = await fetchRecordings(root, { userId, orgId, startTime: timeRange.startTime, endTime: timeRange.endTime, token, }); ``` The project documentation also states that matching the body identifiers to JWT claims is only recommended rather than enforced: ```md - body `userId`/`orgId` 宜与 JWT 一致(建议网关校验) ``` ### Technical Analysis The caller controls `userId` and `orgId`, while the authentication tok ...[truncated 1443 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Derive `userId` and `orgId` from verified token claims at a trusted gateway rather than accepting them as arbitrary request fields. - Reject any supplied identifier that does not exactly match the authenticated principal's authorized scope. - Require the backend to enforce tenant-scoped object-level authorization for every request; do not rely solely on this script or gateway validation. - Use a dedicated, narrowly scoped endpoint that infers tenant identity from authentication context. - Avoid accepting tokens through positional command-line arguments because they may be exposed through process listings or shell history. - Add negative authorization tests covering cross-user and cross-organization requests. - Log denied tenant mismatches without logging bearer tokens or transcript content. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/resolve-user-intent.mjs:106
Finding
Confirmed Time-Range Override Bypasses Query Scope Limits<![CDATA[ ## Vulnerability Details **File Location**: `scripts/resolve-user-intent.mjs:106-121` **Vulnerability Type**: Unvalidated authorization and data-scope parameter **Risk Level**: Medium ### Vulnerable Code ```js function applyConfirmedTime(timeRange, confirmedTime) { if (confirmedTime == null) return timeRange; if (typeof confirmedTime === "string" && /^default$/i.test(confirmedTime.trim())) { const parsed = parseTimeRange(""); return { ...parsed, confidence: "explicit", source: "default", label: "用户确认:默认近 1 个月" }; } if (typeof confirmedTime === "object" && confirmedTime.startTime && confirmedTime.endTime) { return { ...timeRange, startTime: String(confirmedTime.startTime), endTime: String(confirmedTime.endTime), confidence: "explicit", source: "user", label: "用户确认时间窗", }; } return timeRange; } ``` ### Technical Analysis Ordinary natural-language time ranges pass through `parse-time-range.mjs`, which clamps days, weeks, and months. In contrast, an object supplied through `confirmedTime` replaces `startTime` and `endTime` verbatim. The override is not checked for syntax, chronological order, future dates, maximum duration, or allowed historical retention. It therefore bypasses the documented upper limits and can broaden the quantity of sensitive data requested. The backend may impose its own limits, but no such enforcement is demonstrated in the audited code. ### Attack Path 1. The caller supplies a normal authenticated request. 2. Instead of relying on the parsed user message, the caller includes a `confirmedTime` object. 3. The caller sets `startTime` to an arbitrarily old date and `endTime` to the current or a future date. 4. `applyConfirmedTime` marks the range explicit without validating or clamping it. 5. The fetch script sends the oversized range to the recording service. 6. If accepted by the backend, records outside the Skill's documented maximum window are returned. ### Impa ...[truncated 406 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse `confirmedTime.startTime` and `confirmedTime.endTime` using a strict, documented timestamp format. - Reject invalid dates, future dates where unnecessary, and ranges where `startTime` is later than `endTime`. - Apply the same maximum duration to confirmed objects as to natural-language input. - Clamp or reject ranges exceeding the permitted retention window; rejection is preferable when silent truncation could mislead users. - Enforce an equivalent maximum range at the backend because client-side restrictions are bypassable. - Add request and response size limits, pagination, and execution timeouts. - Add tests for malformed timestamps, reversed ranges, future ranges, and multi-year ranges. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/fetch-asr-recordings.mjs:89
Finding
Entire Backend Recording Objects Are Exposed Instead of an Explicit Field Allowlist<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-asr-recordings.mjs:89-99` and `scripts/fetch-asr-recordings.mjs:183-233` **Vulnerability Type**: Excessive sensitive-data exposure **Risk Level**: Medium ### Vulnerable Code ```js function normalizeRecords(apiBody) { const data = apiBody?.data; if (Array.isArray(data)) { return data; } if (data && Array.isArray(data.records)) { return data.records; } return []; } ``` ```js const records = normalizeRecords(apiBody); const combinedAsrText = records.map((r) => r?.asrText ?? "").join("\n"); const themeRelated = analysisFocus.explicit ? isThemeRelatedToTranscripts(combinedAsrText, analysisFocus) : true; const payload = { ok: res.ok && apiBody?.code === 0, httpStatus: res.status, apiCode: apiBody?.code, apiMsg: apiBody?.msg, baseUrl: root, httpMethod: method, userMessage: intent.userMessage, outputFormat, query: { userId, orgId, startTime: timeRange.startTime, endTime: timeRange.endTime, timeZone: timeRange.timeZone, timeSource: timeRange.source, timeConfidence: timeRange.confidence, timeLabel: timeRange.label, timeNotice: reportPlan.timeNotice, }, analysisFocus, themeRelated, reportPlan: { ...reportPlan, themeUnrelated: analysisFocus.explicit && !themeRelated, themeUnrelatedMessage: analysisFocus.explicit && !themeRelated ? `在所查时间窗内,转写内容未涉及「${analysisFocus.theme ?? analysisFocus.rawPhrase}」,仅输出:无相关内容。` : null, }, recordCount: records.length, recordsTruncated: records.length > MAX_RECORDS_HINT, recordsHint: records.length > MAX_RECORDS_HINT ? `记录数 ${records.length} 超过建议上限 ${MAX_RECORDS_HINT},分析时请优先近期样本并抽样摘录。` : null, records, }; console.log(JSON.stringify(payload)); ``` ### Technical Analysis The declared analysis requirement states that conclusions should use only `asrText`, but the implementation passes each complete backend record through to stdou ...[truncated 1448 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Transform each record using an explicit allowlist before any downstream processing, for example: ```js const records = normalizeRecords(apiBody).map((record) => ({ recordingId: record.recordingId, recordedAt: record.recordedAt, asrText: String(record.asrText ?? ""), })); ``` - Include only fields demonstrably required for report generation. - Explicitly exclude `aiJiaolianResultJson`, `aiSummaryContent`, credentials, storage URLs, internal metadata, and unknown properties. - Apply actual record-count, transcript-length, and total-output-size limits rather than returning only a warning. - Prefer pagination or bounded sampling before printing data. - Review stdout retention and access controls because transcripts themselves are sensitive. - Add schema tests that fail if unexpected backend fields appear in Skill output. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
scripts/fetch-asr-recordings.mjs:176
Finding
Raw Non-JSON Backend Responses Are Written to Error Logs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch-asr-recordings.mjs:176-181` **Vulnerability Type**: Unbounded sensitive error-response disclosure **Risk Level**: Low ### Vulnerable Code ```js const bodyText = await res.text(); let apiBody; try { apiBody = JSON.parse(bodyText); } catch { console.error(JSON.stringify({ ok: false, httpStatus: res.status, raw: bodyText })); process.exit(2); } ``` ### Technical Analysis When the backend returns a response that is not valid JSON, the script copies the entire response body to stderr without redaction or a size limit. Error pages from APIs, reverse proxies, authentication systems, or attacker-controlled endpoints may contain internal diagnostics, reflected request data, personal information, or secrets. Because stderr is commonly retained by orchestration platforms and centralized log services, this behavior can convert a transient response into a persistent disclosure. An excessively large response can also increase memory and logging resource usage because `res.text()` buffers the entire body. ### Attack Path 1. The backend, an intermediary, or an attacker-controlled configured endpoint returns a non-JSON response. 2. The response contains sensitive diagnostics, reflected values, or a very large body. 3. JSON parsing fails. 4. The script serializes the full `bodyText` under the `raw` property. 5. Process supervisors, CI systems, Agent infrastructure, or centralized logging services retain the content. ### Impact Assessment The exposed scope is the full non-JSON response body. Potential consequences include disclosure of internal service details, personal information, reflected credentials, and excessive log consumption. This finding does not by itself grant additional system privileges. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Do not include raw response bodies in normal error output. - Return a bounded diagnostic containing only the HTTP status, content type, request ID, and a generic parse-error message. - If a response excerpt is operationally necessary, truncate it to a small fixed length and redact credentials, tokens, cookies, and personal data. - Enforce response-size limits while streaming rather than calling `res.text()` without a bound. - Store detailed diagnostics only in an access-controlled debug channel that is disabled by default. - Ensure production logging systems apply retention limits and sensitive-data filters. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (13)

Ae1

High
Category
analysis-evasion
Content
**禁止只跑 `fetch-asr-recordings.mjs` 而不先意图解析**(fetch 内置相同解析,若需反问会直接 `ok:false` 且不拉数)。
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
## 时间规则(`parse-time-range.mjs`)
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill explicitly documents use of environment variables and network access (`LEGION_AUTH_TOKEN`, `LEGION_HARDWARE_BASE_URL`, and calls to an internal HTTP service), but it does not declare any tool scope or allowed-tools boundary. This creates an overbroad execution surface where the runtime may grant undeclared capabilities, reducing reviewability and increasing the risk of unintended data access or network exfiltration if the skill or adjacent components are modified.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The skill processes ASR transcript content and sends data to an internal analysis service, but the user-facing description does not clearly warn that conversation-derived transcript data will be transmitted for backend analysis. This weakens informed consent and can expose sensitive customer or employee conversation content to internal systems without adequate transparency, which is especially concerning given the reporting and summarization workflow over recordings.

Missing User Warnings

Medium
Confidence
88% confidence
Finding
This code sends userId, orgId, time-range parameters, and a bearer token to a remote HTTP endpoint, but the script provides no confirmation prompt or user-facing disclosure before performing the request. The only visible messages are error responses for missing inputs, not warnings that sensitive identifiers and credentials will be transmitted over the network.

Natural-Language Policy Violations

Medium
Confidence
89% confidence
Finding
This file defines the skill’s analysis themes, keywords, and documentation entirely in Chinese, including user-interpreted labels such as "股票趋势", "拓客复盘", and "销售话术". Because the skill recognizes and returns Chinese-only themes without offering a language choice or documenting a justified region-specific restriction, it creates a locale policy issue.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
The file’s comments and parsing logic indicate the skill is designed around Chinese phrasing and a fixed Asia/Shanghai locale, and the returned result also hard-codes that timezone. Under the policy, forcing a specific language or locale without offering user choice or documenting a justified region-specific constraint is a natural-language policy violation.

Context-Inappropriate Capability

Medium
Confidence
91% confidence
Finding
The resolver appends process.env.LEGION_USER_MESSAGE to the effective user input, meaning behavior can be influenced by out-of-band data not supplied in the current request. This creates an input-smuggling channel that can alter parsed time range, analysis focus, or output format, undermining trust in intent resolution and potentially causing unauthorized or misleading actions.

Context-Inappropriate Capability

Medium
Confidence
94% confidence
Finding
Clarification can be globally bypassed via process.env.LEGION_SKIP_CLARIFICATION === "1", which contradicts the skill’s described ask-first workflow and allows execution with defaults even when time range or analysis dimensions are missing or ambiguous. In practice this can silently suppress a safety/accuracy control and lead to data retrieval or reporting based on assumptions the user never confirmed.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The file-level comment and user-facing error strings are written only in Chinese, indicating an implicit language constraint with no opt-in or alternative locale handling. Under the policy, forcing a specific language without user choice or documented justification is a natural-language policy concern.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
This file includes natural-language comments and usage guidance only in Chinese ("仅解析用户意图(不拉数)" and "用法"). Under the policy, forcing a specific language without user opt-in can be a locale/language policy violation, and there is no indication here that the skill offers an alternative language or that the constraint is justified.

Natural-Language Policy Violations

Low
Confidence
89% confidence
Finding
The file-level description and all user-facing clarification strings are written exclusively in Chinese, indicating the skill expects or enforces Chinese-language interaction. There is no natural-language indication that users may choose another language or locale, which can violate language/locale policy requirements when no opt-in is provided.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
This markdown template contains all user-facing text in Chinese, including headings, labels, and footer content. Under the policy rule for language/locale, forcing a specific language without opt-in or clear justification is a natural-language policy concern.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
scripts/fetch-asr-recordings.mjs:9