Back to skill

Security audit

Xero Cli

Security checks for vulnerabilities and agentic risk

Overview

This Xero accounting skill is coherent and not malicious, but it needs review because it can change financial records, upload local files, and stores tokens with weak controls.

Install only if you are comfortable granting an agent write-capable access to Xero. Use a test organization first, restrict Xero OAuth scopes where possible, avoid production credentials until dependencies and runtime are pinned, secure or relocate the token file, and require explicit user approval before any create, update, delete, void, allocation, or file-attachment command.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (3)

T08 · Insecure Dependencies

Warning
Location
SKILL.md:14
Finding
Unpinned Runtime and Application Dependencies Permit Supply-Chain Code Substitution<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:14-17`; `scripts/package.json:5-9` **Vulnerability Type**: Unpinned runtime execution and mutable dependency resolution **Risk Level**: Medium ### Vulnerable Code ```markdown **Agent:** Determine this SKILL.md file's directory as `SKILL_DIR`, then run commands with: ```bash npx -y bun ${SKILL_DIR}/scripts/cli.ts <command> ``` ``` ```json "dependencies": { "commander": "^12.1.0", "express": "^4.21.0", "open": "^10.1.0", "xero-node": "^9.3.0" } ``` ### Technical Analysis The documented invocation uses `npx -y bun`, which allows the package manager to download and execute the version of the `bun` package resolved at runtime without interactive confirmation. The runtime version is not pinned. The application dependencies also use caret ranges, and the audited project contains no lockfile. Consequently, a later installation may resolve dependency versions different from those reviewed during this audit. Because npm package lifecycle and runtime code execute with the permissions of the invoking process, a compromised or unexpectedly modified dependency could access the process environment, local files, stored OAuth tokens, and network resources. This does not establish that any currently declared package is malicious. The vulnerability is the lack of reproducible and integrity-constrained dependency resolution. ### Attack Path 1. An attacker compromises a package, maintainer account, registry distribution channel, or newly resolved dependency version. 2. The user or Agent executes the documented `npx -y bun` command. 3. `npx` automatically downloads and executes the runtime package without confirmation, or the package manager installs a newer dependency allowed by a caret range. 4. The malicious package code executes in the CLI process. 5. The code can read `XERO_CLIENT_ID`, `XERO_CLIENT_SECRET`, the OAuth token file, and any files accessible to the current operating-system account. 6. ...[truncated 775 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Replace `npx -y bun` with a trusted, preinstalled Bun executable or pin the runtime to a reviewed exact version. 2. Pin every application dependency to an exact version rather than using caret ranges. 3. Generate and commit the appropriate lockfile. 4. Enforce frozen or immutable lockfile installation in deployment and execution workflows. 5. Use a trusted package registry and, where supported, validate package integrity hashes and signatures. 6. Disable unnecessary package lifecycle scripts during installation. 7. Run the CLI under a dedicated, least-privileged operating-system account. 8. Add automated dependency vulnerability and provenance scanning to release workflows. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth/token-store.ts:18
Finding
OAuth Access and Refresh Tokens Are Stored Without Enforced Restrictive Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth/token-store.ts:18-34` **Vulnerability Type**: Plaintext sensitive-token storage with permissions inherited from the process umask **Risk Level**: Medium ### Vulnerable Code ```ts constructor(tokenPath?: string) { this.tokenPath = tokenPath || path.join(__dirname, '../../data/tokens.json'); } save(tokenSet: TokenSet, activeTenantId?: string): void { const data: StoredTokens = { tokenSet, activeTenantId, updatedAt: new Date().toISOString(), }; const dir = path.dirname(this.tokenPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(this.tokenPath, JSON.stringify(data, null, 2), 'utf-8'); } ``` ### Technical Analysis The token store serializes the complete Xero token set, including reusable access and refresh tokens, to a plaintext JSON file. Neither `mkdirSync` nor `writeFileSync` specifies a restrictive permission mode. The effective directory and file permissions therefore depend on the process umask and existing filesystem permissions. On a multi-user host or a system with a permissive umask, another local account may be able to read the token file. If the file already exists with insecure permissions, rewriting it does not automatically correct those permissions. The implementation resolves the default token path to `scripts/data/tokens.json` relative to the module directory, while `SKILL.md:236` states that tokens are stored in `data/tokens.json` in the working directory. This discrepancy can cause administrators to secure, back up, inspect, or delete the wrong path. ### Attack Path 1. A user completes the OAuth login process. 2. The application writes the access and refresh tokens to `scripts/data/tokens.json`. 3. The host has a permissive umask, the token directory is shared, or the file already has overly broad permissions. 4. Another local user or process reads the JSON token file. 5. The attacker extracts th ...[truncated 1013 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer an operating-system credential store or encrypted secret-management service instead of a plaintext JSON file. 2. If file storage is required, create the token directory with mode `0700`. 3. Create or atomically replace the token file with mode `0600`. 4. Check and repair permissions on existing directories and token files before loading them. 5. Reject symbolic links and verify that the resolved token path is owned by the expected user. 6. Write to a securely created temporary file in the same directory, flush it, and atomically rename it to avoid partial writes and unsafe replacement. 7. Correct `SKILL.md` to document the actual token location, or modify the implementation so that it matches the documented location. 8. Revoke Xero tokens during logout where supported, rather than only deleting the local copy. 9. Avoid placing token material under a project directory that may be archived, synchronized, or committed. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/auth/oauth-server.ts:13
Finding
OAuth Callback Server Is Not Explicitly Restricted to Loopback and Renders Dynamic Values Without HTML Escaping<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auth/oauth-server.ts:13-21`, `scripts/auth/oauth-server.ts:56`, `scripts/auth/oauth-server.ts:92`, `scripts/auth/oauth-server.ts:115` **Vulnerability Type**: Exposed temporary listener and unsafe HTML generation **Risk Level**: Medium ### Vulnerable Code ```ts export function startOAuthServer(xero: XeroClient): Promise<OAuthResult> { return new Promise((resolve) => { const app = express(); let server: Server; app.get('/callback', async (req: Request, res: Response) => { try { const callbackUrl = `http://localhost:${PORT}${req.url}`; const tokenSet = await xero.apiCallback(callbackUrl); ``` ```ts ${tenants.length > 0 ? `<p>Connected to: <strong>${tenants[0].tenantName}</strong></p>` : ''} ``` ```ts <code>${error}</code> ``` ```ts server = app.listen(PORT, () => {}); ``` ### Technical Analysis Calling `app.listen(PORT)` without a host does not explicitly constrain the temporary OAuth service to `127.0.0.1` or `::1`. Depending on the Node.js runtime and host networking configuration, the listener may accept connections through non-loopback interfaces. Although OAuth callback processing is expected to rely on state validation performed by `xero-node`, unnecessary network exposure still enlarges the attack surface of a service intended only for the local browser. The success page interpolates `tenantName` into HTML, and the failure page interpolates an SDK or provider-derived error string into HTML. Neither value is HTML-escaped. If attacker-controlled markup reaches either value, the browser will interpret it as HTML rather than display it as text. The error path is particularly exposed because callback query data is passed to `xero.apiCallback`, and returned error messages may include provider or SDK content. The source reviewed here delegates OAuth state validation to `xero-node`; this audit does not claim that state validation is absent. The confirmed co ...[truncated 1868 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Explicitly bind the server to loopback: ```ts server = app.listen(PORT, '127.0.0.1', () => {}); ``` 2. If IPv6 localhost support is required, implement an explicit and tested loopback-only strategy. 3. Render dynamic data through a template engine with automatic escaping, or apply a robust HTML-escaping function before interpolation. 4. Do not expose raw SDK response bodies or exception messages in browser responses. Return a generic error and log a sanitized diagnostic locally. 5. Add restrictive response headers, including a Content Security Policy such as `default-src 'none'; style-src 'unsafe-inline'`, along with `X-Content-Type-Options: nosniff`. 6. Continue using cryptographically strong OAuth state and PKCE through the SDK, and add tests confirming that mismatched or missing state is rejected. 7. Accept only the expected callback path and required query parameters. 8. Close the server on all completion, timeout, and error paths, and add a short authentication timeout. 9. Prevent concurrent login sessions from sharing the same fixed callback port and state. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • 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 (28)

Lp3

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding
The skill requests sensitive Xero credentials via environment variables but does not declare an explicit tool/permission scope in the manifest. That weakens least-privilege controls and can cause an agent platform to expose secrets or execution capabilities more broadly than intended when running financial-accounting operations.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The skill instructs the agent to run `npx -y bun` without a pinned version, which allows retrieval of the latest package at execution time. This creates a supply-chain risk where a compromised, malicious, or breaking upstream release could execute arbitrary code in the agent environment while handling Xero credentials and OAuth tokens.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
This invocation again uses `npx -y bun` without version pinning, permitting runtime installation of an untrusted latest package. Because the command is used for OAuth authentication, compromise here could expose Xero client secrets, access tokens, refresh tokens, or redirect the auth flow.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The unpinned `npx -y bun` reference introduces the same supply-chain execution risk at token-status verification time. In this skill's context, any arbitrary code execution can access local token files and financial data, making the exposure more serious than a generic developer utility.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
Using `npx -y bun` as the documented command prefix means nearly every operation depends on fetching or invoking an unpinned external runtime. A compromised package or registry response could execute arbitrary code before invoice, payment, contact, or bank-transaction commands run.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
This example command uses the same unpinned runtime fetch in a workflow that queries contacts. Although read-oriented, arbitrary code execution here could still harvest Xero credentials, OAuth tokens, or returned accounting data.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The accounts-list example relies on `npx -y bun` without version pinning, preserving a supply-chain execution path. Since the skill operates on accounting metadata and may have authenticated access already present, compromise could leak account structures or stage later fraudulent write actions.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
This invoice-creation example couples unpinned runtime execution with a write-capable financial action. If the runtime source is compromised, an attacker could alter invoice contents, exfiltrate customer/account data, or persist access via stolen OAuth tokens.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
Authorizing an invoice through an unpinned `npx -y bun` command is especially sensitive because it changes business state. Arbitrary code execution at this point could modify financial approvals, tamper with request payloads, or steal authentication material.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The payment-creation example uses an unpinned runtime in a workflow that can mark invoices paid. In the context of accounting software, this raises the impact because arbitrary code execution could falsify payment records, access bank-account identifiers, or exfiltrate tokens.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
This is another unpinned `npx -y bun` invocation in the payment workflow, preserving the same supply-chain risk. Because it performs a write action against accounting records, the business impact of compromise includes fraud, record tampering, and credential theft.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
The contact-creation example still executes through an unpinned external runtime. Arbitrary code execution here could create fraudulent contacts, alter bookkeeping context, and steal sensitive customer or tenant information from the authenticated environment.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
Creating bank transactions via an unpinned `npx -y bun` command is high risk because it combines supply-chain exposure with direct financial record manipulation. A compromised runtime could submit unauthorized transactions or exfiltrate banking and token data.

Rp1

Medium
Category
MCP Rug Pull
Confidence
97% confidence
Finding
Attaching files to transactions through an unpinned runtime retains the same code-execution risk, with added exposure to local file contents. In this context, a compromised runtime could read arbitrary local files, upload unintended documents, or harvest tokens stored on disk.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
This code persists the OAuth token set and active tenant identifier via tokenStore.save, which is a sensitive write involving authentication credentials. While there is success/error messaging later, there is no confirmation prompt, explicit user-facing notice, or inline comment/docstring here explaining that tokens will be stored locally.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The save method persists the full TokenSet and tenant identifier to a local JSON file, which is a safety-relevant file write involving authentication material. There is no confirmation prompt, user-facing log message, or explanatory comment/docstring warning that sensitive tokens are being stored on disk.

Missing User Warnings

Medium
Confidence
82% confidence
Finding
The load method reads previously stored TokenSet data from the filesystem, which constitutes access to sensitive credential material. The code provides no user-visible notice, logging, or inline documentation indicating that local auth tokens are being loaded and used.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The attach command reads an arbitrary local file path and uploads its contents to Xero without any confirmation, path restriction, or explicit disclosure warning at the point of use. In an agent skill context, this creates a real data-exfiltration risk because a caller can cause sensitive local files to be transmitted to a remote third-party API under the user's authenticated account.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This code issues a network API call that creates a new invoice in an external accounting system, which is a safety-relevant write operation affecting business data. Although the command name implies creation, there is no confirmation prompt, user-facing warning, or inline disclosure near the action itself about the remote side effect.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The updateInvoice call modifies an existing invoice in Xero and may set its status to VOIDED, which is potentially irreversible or business-critical. The code does not provide any confirmation prompt or explicit warning before performing this external state-changing action.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The `delete` command performs an irreversible or business-critical state change by calling `xero.accountingApi.deletePayment` immediately from a single CLI invocation, with no confirmation prompt, dry-run, or explicit force flag. In an accounting context, accidental deletion/voiding of payments can corrupt financial records, disrupt reconciliation, and cause operational or audit issues even if the caller is already authenticated.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The clear method irreversibly deletes the token storage file using fs.unlinkSync, which is a destructive file operation. There is no confirmation prompt, user-facing message, or explanatory comment indicating that stored authentication state will be removed.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"private": true,
  "type": "module",
  "dependencies": {
    "commander": "^12.1.0",
    "express": "^4.21.0",
    "open": "^10.1.0",
    "xero-node": "^9.3.0"
Confidence
95% confidence
Finding
The dependency uses a caret range instead of an exact version, which allows different package versions to be installed over time. This weakens build reproducibility and can unexpectedly introduce vulnerable or malicious upstream releases through normal dependency resolution.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"type": "module",
  "dependencies": {
    "commander": "^12.1.0",
    "express": "^4.21.0",
    "open": "^10.1.0",
    "xero-node": "^9.3.0"
  }
Confidence
98% confidence
Finding
The express dependency is not pinned to an exact version, so installations may resolve to different releases within the allowed range. In a skill that exposes web/OAuth flows, this increases supply-chain and patch-state uncertainty and may pull in a vulnerable release without code changes.

Unverifiable Dependency: express has 5 known advisory(ies) (CVE-2024-10491 (Express ressource injection); CVE-2014-6393 (No Charset in Content-Type Header in express); CVE-2024-9266 (Express Open Redirect vulnerability) +2 more), but the manifest does not pin a version, so it is unknown whether the installed release is affected

Low
Category
Supply Chain
Confidence
89% confidence
Finding
Express has known advisories, and because the manifest does not pin the version, it is not possible to verify whether the installed release is affected. In this skill, express likely supports OAuth callback or local web endpoints, so an affected version could expose redirect, header handling, or request-processing weaknesses in a security-relevant path.

Static analysis

No suspicious patterns detected.