Back to skill

Security audit

Sales Dashboard

Security checks for vulnerabilities and agentic risk

Overview

The skill has a coherent sales-dashboard purpose, but it needs review because it can use broad local credential files and can prepare arbitrary local file contents for Discord delivery.

Review before installing. Run it only in a dedicated workspace or service account with access limited to the intended OKKI CRM config, report directory, and optional dashboard inputs. Confirm Discord delivery and cron schedules are wanted, avoid passing arbitrary paths to --report or --output, and require HTTPS/approved hosts for CRM authentication.

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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/discord-push.js:123
Finding
Arbitrary Local File Disclosure Through Discord Push Instructions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/discord-push.js`, lines 123-129 and 152-166 **Vulnerability Type**: Unrestricted local file read followed by external-delivery instruction generation **Risk Level**: High ### Vulnerable Code ```js } else if (opts.report) { // 指定报告文件 if (!fs.existsSync(opts.report)) { console.error(`报告文件不存在: ${opts.report}`); process.exit(1); } content = fs.readFileSync(opts.report, 'utf-8'); } ``` ```js const chunks = splitMessage(content); const instructions = generatePushInstructions(chunks, pushType); if (opts.dryRun) { console.log('[DRY-RUN] 推送预览:'); console.log(JSON.stringify(instructions, null, 2)); return; } // 输出 JSON 指令供 agent 读取 console.log(JSON.stringify(instructions)); ``` The generated instruction identifies the requested operation as a Discord push: ```js const instructions = { action: 'discord_push', type, channel: '#🧠-hq-指挥中心', chunks: chunks.map((c, i) => ({ index: i + 1, total: chunks.length, content: c })), total_chunks: chunks.length, generated_at: new Date().toISOString() }; ``` ### Technical Analysis The `--report` argument is treated as an unrestricted filesystem path. The script only checks whether the path exists and then reads it with the privileges of the Node.js process. It does not require the target to be beneath `data/reports`, verify that it is a regular Markdown report, reject symbolic links, or enforce an approved filename pattern. The complete file contents are split into chunks and embedded in an Agent-consumable `discord_push` instruction. Although this script does not directly connect to Discord, its declared integration model expects another Agent component to execute the generated instruction. Therefore, the unrestricted local read can become an external data-disclosure channel. ### Attack Path 1. An attacker or untrusted workflow invokes the script with a sensitive readable path: ```bash node scripts/discord-pu ...[truncated 982 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve the requested path and require it to remain under `REPORTS_DIR`: ```js const reportsRoot = fs.realpathSync(REPORTS_DIR); const requested = fs.realpathSync(path.resolve(REPORTS_DIR, opts.report)); if ( requested !== reportsRoot && !requested.startsWith(reportsRoot + path.sep) ) { throw new Error('Report path is outside the approved report directory'); } ``` 2. Require an approved filename pattern, such as: ```js /^(weekly|monthly)-\d{4}-\d{2}-\d{2}\.md$/ ``` 3. Use `fs.lstatSync()` and reject symbolic links and non-regular files. 4. Accept a report identifier or basename rather than an arbitrary path. 5. Require explicit authorization or confirmation before report content is transmitted externally. 6. Apply content-size limits and optionally scan generated reports for credentials or other sensitive patterns before constructing push instructions. ]]>

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/data-collector.js:28
Finding
Excessive Access to External Environment Files and Token Caches<![CDATA[ ## Vulnerability Details **File Location**: `scripts/data-collector.js`, lines 28-31, 88-117, and 123-127 **Vulnerability Type**: Overbroad secret-file access and configurable access outside the Skill directory **Risk Level**: Medium ### Vulnerable Code ```js const OKKI_WORKSPACE = process.env.OKKI_WORKSPACE || path.resolve(__dirname, '../../../xiaoman-okki'); const OKKI_CONFIG_PATH = path.join(OKKI_WORKSPACE, 'api/config.json'); const OKKI_TOKEN_CACHE = path.join(OKKI_WORKSPACE, 'api/token.cache'); const ENV_PATH = process.env.ENV_PATH || path.resolve(__dirname, '../../../.env'); ``` ```js function loadEnv() { if (!fs.existsSync(ENV_PATH)) return; const lines = fs.readFileSync(ENV_PATH, 'utf-8').split('\n'); for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const idx = trimmed.indexOf('='); if (idx < 0) continue; const key = trimmed.slice(0, idx).trim(); let val = trimmed.slice(idx + 1).trim(); if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { val = val.slice(1, -1); } if (key && val && !process.env[key]) process.env[key] = val; } } ``` ```js if (!forceRefresh && fs.existsSync(OKKI_TOKEN_CACHE)) { try { const cached = JSON.parse(fs.readFileSync(OKKI_TOKEN_CACHE, 'utf-8')); if (cached.expires_at > Date.now() / 1000 + 300) { return cached.access_token; } } catch (_) {} } ``` ### Technical Analysis The collector reads an `.env` file outside the Skill directory and imports every parsed variable into `process.env`, even though its declared task only requires a limited set of OKKI credentials. This exposes the running Skill to unrelated secrets stored in the shared environment file. The `ENV_PATH` and `OKKI_WORKSPACE` environment variables also permit callers to redirect the collector to other accessible filesystem locations. The collector subsequently reads configuration and a ...[truncated 1397 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove generic loading of every `.env` entry. 2. Read only explicitly required variables, such as the approved OKKI client ID, client secret, scope, and endpoint values. 3. Store CRM credentials in a dedicated secret provider rather than a shared workspace `.env`. 4. Remove arbitrary `ENV_PATH` and `OKKI_WORKSPACE` overrides unless they are operationally necessary. 5. If overrides are required, resolve and validate them against an administrator-configured allowlist of trusted directories. 6. Verify external configuration and cache files are regular files, are not symbolic links, and have restrictive ownership and permissions. 7. Run the Skill under a dedicated service account that cannot read unrelated application secrets. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/data-collector.js:132
Finding
CRM Credentials Can Be Sent to an Unvalidated or Plaintext Token Endpoint<![CDATA[ ## Vulnerability Details **File Location**: `scripts/data-collector.js`, lines 132-166 **Vulnerability Type**: Unvalidated credential destination, plaintext HTTP support, and insufficiently protected token storage **Risk Level**: High ### Vulnerable Code ```js const postData = new URLSearchParams({ client_id: config.clientId, client_secret: config.clientSecret, grant_type: 'client_credentials', scope: config.scope }).toString(); const tokenData = await httpRequest(config.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: postData }); tokenData.expires_at = Date.now() / 1000 + (tokenData.expires_in || 7200); fs.writeFileSync(OKKI_TOKEN_CACHE, JSON.stringify(tokenData)); return tokenData.access_token; ``` ```js function httpRequest(url, opts = {}) { return new Promise((resolve, reject) => { const u = new URL(url); const mod = u.protocol === 'https:' ? https : http; const reqOpts = { hostname: u.hostname, port: u.port, path: u.pathname + u.search, method: opts.method || 'GET', headers: opts.headers || {}, timeout: 30000 }; ``` ### Technical Analysis The OAuth client secret is sent to `config.tokenUrl`, which is obtained from an external configuration file. The code does not enforce an allowlist of official OKKI hosts and accepts both HTTPS and plaintext HTTP. If the configuration is modified, replaced, or redirected through the configurable workspace path, credentials can be transmitted to an attacker-controlled host. When HTTP is used, network observers can capture the client ID and secret in transit. Even with HTTPS, the absence of destination allowlisting permits a malicious but valid HTTPS endpoint to receive the credentials. The complete token response is then written to `token.cache` without an explicit restrictive file mode. Its effective permissions depend on the process umask, which may expose access tokens or other respo ...[truncated 1139 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require `https:` for all authentication and API URLs: ```js const tokenUrl = new URL(config.tokenUrl); if (tokenUrl.protocol !== 'https:') { throw new Error('The token endpoint must use HTTPS'); } ``` 2. Allowlist the exact official token hostname and, where practical, the expected port and path. 3. Treat endpoint configuration as trusted administrator configuration and prevent untrusted callers from redirecting its location. 4. Reject redirects or validate the destination of every redirect before forwarding credentials. 5. Write only required cache fields and use restrictive permissions: ```js fs.writeFileSync( OKKI_TOKEN_CACHE, JSON.stringify({ access_token: tokenData.access_token, expires_at: tokenData.expires_at }), { mode: 0o600 } ); ``` 6. Ensure the cache directory is owned by the dedicated service account and is not shared. 7. Rotate the CRM client secret if configuration integrity may already have been compromised. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/metrics-calculator.js:190
Finding
Arbitrary File Overwrite Through the Metrics Output Argument<![CDATA[ ## Vulnerability Details **File Location**: `scripts/metrics-calculator.js`, lines 27-32 and 190-194 **Vulnerability Type**: Unrestricted user-controlled output path **Risk Level**: Medium ### Vulnerable Code ```js function parseArgs() { const args = process.argv.slice(2); const opts = { checkAlerts: false, output: OUTPUT_DEFAULT }; for (let i = 0; i < args.length; i++) { if (args[i] === '--check-alerts') opts.checkAlerts = true; if (args[i] === '--output' && args[i + 1]) opts.output = args[++i]; } return opts; } ``` ```js const outDir = path.dirname(opts.output); if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true }); fs.writeFileSync(opts.output, JSON.stringify(metrics, null, 2)); console.log(`\n已保存: ${opts.output}`); ``` ### Technical Analysis The `--output` argument is accepted as an arbitrary path and passed to `path.dirname()`, `fs.mkdirSync()`, and `fs.writeFileSync()` without confinement to the project data directory. Existing files are overwritten by default. A caller can therefore cause the process to create directories and overwrite any writable path with calculated JSON data. The attacker cannot choose arbitrary file contents through this interface, but predictable generated JSON can still corrupt application files or replace security-relevant files that accept JSON. ### Attack Path 1. The attacker identifies a target path writable by the process account. 2. The attacker invokes: ```bash node scripts/metrics-calculator.js --output /writable/target.json ``` 3. The script creates missing parent directories recursively where permissions permit. 4. `fs.writeFileSync()` truncates and overwrites the selected file with calculated metrics. 5. The affected application or workflow later consumes the corrupted or replaced file. ### Impact Assessment The vulnerability permits file creation and overwrite with the privileges of the Node.js process. It can cause data loss, configuration corruption, denia ...[truncated 333 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `--output` if arbitrary destinations are not required. 2. Otherwise, resolve the requested path and require it to remain beneath an approved data directory. 3. Reject absolute paths, traversal components, symbolic links, and non-JSON extensions. 4. Use a fixed allowlist of supported output filenames. 5. Consider exclusive file creation or explicit overwrite confirmation where historical data should not be replaced. 6. Run with a dedicated account whose write permissions are limited to the Skill's data directory. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/report-generator.js:154
Finding
Report Filename Path Traversal Through Unvalidated Period and Date Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/report-generator.js`, lines 26-33 and 154-162 **Vulnerability Type**: Path traversal and generated-content file overwrite **Risk Level**: Medium ### Vulnerable Code ```js function parseArgs() { const args = process.argv.slice(2); const opts = { period: 'weekly', date: null, dryRun: false }; for (let i = 0; i < args.length; i++) { if (args[i] === '--period' && args[i + 1]) opts.period = args[++i]; if (args[i] === '--date' && args[i + 1]) opts.date = args[++i]; if (args[i] === '--dry-run') opts.dryRun = true; } return opts; } ``` ```js if (!fs.existsSync(REPORTS_DIR)) fs.mkdirSync(REPORTS_DIR, { recursive: true }); const dateStr = opts.date || new Date().toISOString().split('T')[0]; const filename = `${opts.period}-${dateStr}.md`; const filepath = path.join(REPORTS_DIR, filename); fs.writeFileSync(filepath, report); console.log(`报告已保存: ${filepath}`); console.log('\n' + report); ``` ### Technical Analysis The `--period` and `--date` arguments are interpolated directly into a filename. Neither argument is validated against its documented domain. Path separators and `..` traversal components can therefore be included in the generated filename. `path.join()` normalizes traversal components but does not enforce that the result remains inside `REPORTS_DIR`. A crafted period or date can consequently produce a path outside the intended report directory. If the destination's parent directory exists and is writable, `fs.writeFileSync()` will overwrite the target with generated Markdown. ### Attack Path 1. The attacker supplies traversal-bearing `--period` or `--date` input. 2. The values are concatenated into `filename` without validation. 3. `path.join(REPORTS_DIR, filename)` normalizes the traversal and resolves to a location outside `data/reports`. 4. If the resulting parent directory exists and the process can write there, `fs.writeFileSync()` creates or truncates the destinat ...[truncated 577 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Allow only the documented period values: ```js if (!['weekly', 'monthly'].includes(opts.period)) { throw new Error('Invalid report period'); } ``` 2. Require dates to match a strict `YYYY-MM-DD` format and verify that they represent a real calendar date. 3. Construct the filename only after validation. 4. Resolve the final path and verify that it remains beneath the canonical `REPORTS_DIR`. 5. Reject path separators, `..`, absolute paths, null bytes, and symbolic-link destinations. 6. Use non-overwriting creation where preserving existing reports is required. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • System Prompt LeakageDirect Leakage, Indirect Extraction, Tool-Based Exfiltration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (19)

Ae1

High
Category
analysis-evasion
Content
node scripts/data-collector.js --period weekly [--date 2026-03-24] [--dry-run]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/data-collector.js --period weekly [--date 2026-03-24] [--dry-run]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/metrics-calculator.js --check-alerts
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/report-generator.js --period weekly [--dry-run]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/report-generator.js --period weekly [--dry-run]
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/discord-push.js --latest-report weekly
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/discord-push.js --latest-report weekly
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
node scripts/discord-push.js --latest-report weekly
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Credential Access

High
Category
Privilege Escalation
Content
const OKKI_WORKSPACE = process.env.OKKI_WORKSPACE || path.resolve(__dirname, '../../../xiaoman-okki');
const OKKI_CONFIG_PATH = path.join(OKKI_WORKSPACE, 'api/config.json');
const OKKI_TOKEN_CACHE = path.join(OKKI_WORKSPACE, 'api/token.cache');
const ENV_PATH = process.env.ENV_PATH || path.resolve(__dirname, '../../../.env');
const SNAPSHOTS_DIR = path.join(BASE_DIR, 'data', 'snapshots');
const LATEST_PATH = path.join(BASE_DIR, 'data', 'latest.json');
const LOGS_DIR = path.join(BASE_DIR, 'logs');
Confidence
84% confidence
Finding
The script is designed to read secrets from a filesystem .env path and an external workspace token/config cache using broad, attacker-influenceable paths from environment variables. In an agent/skill context, this creates credential exposure risk because the skill can access sensitive local files outside its own directory and then use those credentials to authenticate to external systems.

Credential Access

High
Category
Privilege Escalation
Content
} catch (_) {}
}

// ============ .env 加载 ============
function loadEnv() {
  if (!fs.existsSync(ENV_PATH)) return;
  const lines = fs.readFileSync(ENV_PATH, 'utf-8').split('\n');
Confidence
91% confidence
Finding
The loadEnv() routine reads a .env file from a path that may point outside the project and imports every key/value into process.env without trust boundaries. In a skill environment, this behavior can harvest unrelated credentials present on the host and makes the skill more dangerous because it intentionally ingests potentially sensitive secrets for later use.

Direct Prompt Extraction

High
Category
System Prompt Leakage
Content
generated_at: new Date().toISOString()
  };
  
  return instructions;
}

// ============ 告警推送 ============
Confidence
85% confidence
Finding
Skill contains instructions that could directly expose system prompts, internal rules, or hidden instructions to users or external parties.

Natural-Language Policy Violations

Medium
Confidence
81% confidence
Finding
All user-facing documentation in the skill is written in Chinese, including the title and operational instructions, with no indication that users may choose another language. Under the stated policy, forcing a specific language without user opt-in can be a natural-language policy violation unless the locale restriction is documented and justified.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill explicitly collects sales data from CRM and related sources, generates reports, and pushes them to Discord, but the description does not warn users that potentially sensitive business data will be transmitted to an external messaging platform. This creates a real risk of unintended data disclosure, especially if users assume the skill only produces local dashboards or internal reports.

Natural-Language Policy Violations

Medium
Confidence
89% confidence
Finding
The description is written only in Chinese ("Farreach 销售仪表盘配置"), indicating a fixed language choice in natural-language configuration. The file does not offer any language/locale option or explain that the skill is intentionally limited to a Chinese-speaking or China-region context.

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
The weekly and monthly reports are hard-coded to the Asia/Shanghai timezone, which imposes a locale-specific behavior. There is no indication that users can choose a timezone or that the skill is explicitly scoped to that region for compliance or business reasons.

Natural-Language Policy Violations

Medium
Confidence
93% confidence
Finding
This code file contains natural-language documentation and user-facing runtime messages exclusively in Chinese, starting with the file header and continuing throughout the script. That imposes a specific language on users/operators without any opt-in or documented locale constraint, which matches the language-policy violation criteria.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
This code file contains natural-language comments and usage text entirely in Chinese, and later emits Chinese-only console messages. The policy requires flagging language or locale constraints when a skill forces a specific language without user opt-in or a documented justification.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The script's visible runtime messages, errors, and alerts are presented only in Chinese, which imposes a language choice on users. There is no mechanism for selecting another locale and no documented justification for a Chinese-only interface.

Natural-Language Policy Violations

Low
Confidence
93% confidence
Finding
The number formatter explicitly uses `toLocaleString('en-US', ...)`, which forces a specific locale for output formatting. This is a natural-language/locale policy concern because the file does not offer user choice or explain why US English formatting is required.

Static analysis

No suspicious patterns detected.