Back to skill

Security audit

Alby Lightning Payments

Security checks for vulnerabilities and agentic risk

Overview

This Lightning payments skill is mostly purpose-aligned, but it includes under-disclosed live wallet operations that can move real funds, including through the package test command.

Review before installing. Use only a dedicated Alby NWC connection with strict spending limits, do not run npm test, and treat ALBY_NWC_URL as a wallet-spending secret. The undocumented scripts/wallet.js should be removed or made non-spending before trusting this package in an agent workflow.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/wallet.js:7
Finding
Live Wallet Operations Are Executed by the Package Test Command<![CDATA[ ## Vulnerability Details **File Location**: `package.json:6-8`; `scripts/wallet.js:7-25` **Vulnerability Type**: Unsafe automatic financial operations **Risk Level**: High ### Vulnerable Code `package.json:6-8`: ```json "main": "scripts/wallet.js", "scripts": { "test": "node scripts/wallet.js" } ``` `scripts/wallet.js:7-25`: ```js // Check balance const { balance } = await client.getBalance(); console.log(`Balance: ${Math.floor(balance / 1000)} sats`); // Pay a BOLT11 invoice await client.payInvoice({ invoice: "lnbc...", amount: 1000 * 1000 // msats explicitly }); // Pay a Lightning address const ln = new LN(process.env.ALBY_NWC_URL); await ln.pay("user@getalby.com", SATS(100)); // Create an invoice to receive const result = await client.makeInvoice({ amount: 2000 * 1000, description: "Payment" }); console.log(result.invoice); ``` ### Technical Analysis The package maps the conventional `npm test` command to `scripts/wallet.js`, but that file is not a test suite. It connects to the wallet identified by the spending-capable `ALBY_NWC_URL` credential and performs operations against that live wallet. The script reads and logs the balance, attempts to pay a BOLT11 invoice, contains a hard-coded 100-satoshi transfer to `user@getalby.com`, and creates a receiving invoice. These operations occur immediately through top-level statements without an explicit payment confirmation, test-mode check, mocked client, or environment guard. The placeholder invoice (`lnbc...`) will ordinarily cause the first payment call to fail and prevent subsequent statements from running. However, an expected failure is not a valid security boundary. If the invoice is replaced with a valid value, accepted unexpectedly by a client implementation, or the execution flow is modified, the hard-coded Lightning-address payment becomes reachable. The README documents only `send_sats.mjs` and `pay_bolt11.mjs` as provided scripts. It does not disclose that ...[truncated 1249 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove live wallet operations from the package test command. 2. Replace the current command with a genuine unit-test suite that uses a mocked `NWCClient` and performs no external network or wallet operations. 3. Remove `scripts/wallet.js` as the package entry point unless it is intended to be a supported executable. 4. Convert demonstration operations into non-executable documentation examples. 5. If a live integration test is necessary, require all of the following: - An explicit opt-in environment variable such as `ALLOW_LIVE_WALLET_TESTS=true`. - A separate NWC credential with a minimal spending allowance. - An explicit recipient and amount supplied by the operator. - A confirmation step before every payment. - A clearly named command such as `npm run test:live-wallet`. 6. Do not log wallet balances in routine tests or CI output. 7. Document every executable script and its financial side effects in the README. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
send_sats.mjs:58
Finding
LNURL Requests Can Reach Private Networks Through DNS Resolution or Rebinding<![CDATA[ ## Vulnerability Details **File Location**: `send_sats.mjs:58-78`, `send_sats.mjs:101-115` **Vulnerability Type**: Server-Side Request Forgery protection bypass **Risk Level**: Medium ### Vulnerable Code `send_sats.mjs:58-78`: ```js /** Validate a callback URL against SSRF risks. Throws on violation. */ function validateCallbackUrl(urlStr, expectedDomain) { let parsed; try { parsed = new URL(urlStr); } catch { throw new Error("Invalid callback URL."); } if (parsed.protocol !== "https:") { throw new Error("Callback URL must use HTTPS."); } if (parsed.hostname !== expectedDomain && !parsed.hostname.endsWith(`.${expectedDomain}`)) { throw new Error(`Callback domain mismatch: expected ${expectedDomain}, got ${parsed.hostname}`); } // Block private/loopback/link-local IPs (IPv4 + IPv6 prefixes), including 0.0.0.0 const privatePattern = /^(0\.0\.0\.0|127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|::1$|fc|fd)/i; if (privatePattern.test(parsed.hostname)) { throw new Error("Callback URL points to a private/internal address. Blocked."); } if (parsed.username || parsed.password) { throw new Error("Callback URL must not contain credentials."); } return parsed; } ``` `send_sats.mjs:101-115`: ```js const client = new NWCClient({ nostrWalletConnectUrl: NWC_URL }); try { // 1. Resolve lightning address → LNURL metadata const lnurlRes = await fetch( `https://${domain}/.well-known/lnurlp/${encodeURIComponent(user)}`, { redirect: "error", signal: AbortSignal.timeout(10_000) } ); if (!lnurlRes.ok) throw new Error(`LNURL lookup failed: HTTP ${lnurlRes.status}`); const lnurlData = await lnurlRes.json(); if (lnurlData.status === "ERROR") throw new Error(`LNURL error: ${lnurlData.reason}`); if (!lnurlData.callback) throw new Error("Invalid LNURL response: no callback URL"); // 2. Validate callback URL (SSRF protection) const callbackUrl = validateCallbackUrl(lnurlData.callb ...[truncated 3248 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Resolve each destination hostname before every network request. 2. Reject the request if any resolved IPv4 or IPv6 address belongs to: - Loopback ranges. - Private-use ranges. - Link-local ranges. - Multicast ranges. - Unspecified or reserved ranges. - IPv4-mapped IPv6 representations of prohibited IPv4 addresses. 3. Apply the same destination policy to both: - The initial Lightning-address metadata request. - The invoice callback request. 4. Prevent DNS rebinding by binding the HTTP connection to the previously validated IP address while retaining the original hostname for TLS Server Name Indication and certificate verification. 5. Revalidate the destination for every new connection rather than relying on a prior DNS result. 6. Continue rejecting redirects, or validate every redirect destination using the same controls if redirects are ever enabled. 7. Prefer a mature, centrally maintained SSRF-resistant request component over regular-expression-based IP filtering. 8. Consider restricting outbound traffic at the operating-system or container level so the Skill cannot connect to private or metadata networks. ]]>

T09 · Insecure Skill Coding Practices

Note
Location
send_sats.mjs:20
Finding
Payment Amounts Use Unsafe Floating-Point Integer Conversion<![CDATA[ ## Vulnerability Details **File Location**: `send_sats.mjs:20-30`, `send_sats.mjs:118-125`, `send_sats.mjs:139-143` **Vulnerability Type**: Improper numeric validation in financial calculations **Risk Level**: Low ### Vulnerable Code `send_sats.mjs:20-30`: ```js // Strict integer parsing — reject "10abc", negative, zero if (!/^[0-9]+$/.test(amountArg)) { console.error("Amount must be a positive integer (sats)."); process.exitCode = 1; process.exit(); } const AMOUNT_SATS = Number(amountArg); if (AMOUNT_SATS <= 0) { console.error("Amount must be greater than 0."); process.exitCode = 1; process.exit(); } ``` `send_sats.mjs:118-125`: ```js // 3. Validate amount against server limits const msats = AMOUNT_SATS * 1000; if (lnurlData.minSendable !== undefined && msats < lnurlData.minSendable) { throw new Error(`Amount too low: minimum is ${lnurlData.minSendable / 1000} sats`); } if (lnurlData.maxSendable !== undefined && msats > lnurlData.maxSendable) { throw new Error(`Amount too high: maximum is ${lnurlData.maxSendable / 1000} sats`); } ``` `send_sats.mjs:139-143`: ```js // 5. Verify invoice amount matches requested amount (prevents overpayment by malicious server) const invoiceMsats = decodeBolt11Msats(invoiceData.pr); if (invoiceMsats !== null && invoiceMsats !== BigInt(msats)) { throw new Error( `Invoice amount mismatch: expected ${msats} msats, got ${invoiceMsats} msats. Aborting.` ); } ``` ### Technical Analysis The script confirms that the amount consists only of decimal digits but does not impose a maximum length or value before converting it to a JavaScript `Number`. JavaScript numbers use IEEE-754 double-precision floating point and cannot exactly represent every integer above `Number.MAX_SAFE_INTEGER`. Large values can therefore be rounded during `Number(amountArg)`. Multiplication by 1,000 can introduce further loss of precision. Extremely large digit strings can become `Infinity`. The later conversion `BigInt(m ...[truncated 1747 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse the command-line amount directly as `BigInt` after digit validation: ```js const amountSats = BigInt(amountArg); if (amountSats <= 0n) { throw new Error("Amount must be greater than 0."); } ``` 2. Enforce a documented maximum amount before making any network request. 3. Perform millisatoshi conversion with exact arithmetic: ```js const amountMsats = amountSats * 1000n; ``` 4. Parse `minSendable` and `maxSendable` into validated integer representations and compare them using `BigInt`. 5. Convert a value back to `Number` only when an external API requires it and only after proving that it is no greater than `Number.MAX_SAFE_INTEGER`. 6. Reject non-integer, non-finite, negative, zero, and out-of-range values explicitly. 7. Require an explicit amount for zero-amount invoices and verify the final payment amount through a wallet API that supports exact millisatoshi values. 8. Add boundary tests covering: - `1`. - The maximum permitted amount. - `Number.MAX_SAFE_INTEGER`. - Values immediately above the safe-integer limit. - Extremely long decimal strings. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Rogue AgentSelf-Modification, Session Persistence
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (7)

Missing User Warnings

High
Confidence
96% confidence
Finding
The script initiates real outgoing Lightning payments programmatically via both invoice payment and Lightning address payment with no confirmation, authorization gate, recipient validation, or spending limit. In an agent or automation context, this can directly cause unauthorized fund transfers if the script is triggered unexpectedly, supplied attacker-controlled payment details, or run with a wallet that has available balance.

Session Persistence

Medium
Category
Rogue Agent
Content
### Installation
```bash
mkdir -p ~/.openclaw/workspace/skills/alby-lightning
cd ~/.openclaw/workspace/skills/alby-lightning
npm init -y
npm install @getalby/sdk
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
### Installation
```bash
mkdir -p ~/.openclaw/workspace/skills/alby-lightning
cd ~/.openclaw/workspace/skills/alby-lightning
npm init -y
npm install @getalby/sdk
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The script directly pays any BOLT11 invoice provided on the command line as soon as it is invoked, with no explicit confirmation, amount display, recipient verification, or policy check before funds are spent. In an agent/skill context, this is risky because upstream tooling or prompt-influenced input could cause unintended real-money payments to attacker-controlled invoices.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This code accesses a sensitive credential-like value via process.env.ALBY_NWC_URL to establish wallet connectivity, but the file provides no user-facing disclosure beyond implementation details. Under the code-file criteria, sensitive environment-variable access should be flagged when there is no confirmation prompt, visible warning, or explanatory documentation in the file.

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Content
try {
  // 1. Resolve lightning address → LNURL metadata
  const lnurlRes = await fetch(
    `https://${domain}/.well-known/lnurlp/${encodeURIComponent(user)}`,
    { redirect: "error", signal: AbortSignal.timeout(10_000) }
  );
Confidence
60% confidence
Finding
Request target host is built from a dynamic or untrusted value. If the host is attacker-influenced, this enables SSRF to arbitrary internal or metadata endpoints.

Unpinned Dependencies

Low
Category
Supply Chain
Content
"license": "MIT",
  "type": "module",
  "dependencies": {
    "@getalby/sdk": "^7.0.0"
  }
}
Confidence
94% confidence
Finding
The dependency is specified with a caret range (`^7.0.0`), which allows automatic installation of future minor and patch releases. This creates supply-chain risk because a compromised upstream release or unexpected breaking/security-relevant behavior change could be pulled in without explicit review, which is particularly relevant here because the package handles Lightning wallet/payment functionality.

Static analysis

Detected: suspicious.env_credential_access

Environment variable access combined with network send.

Critical
Code
suspicious.env_credential_access
Location
send_sats.mjs:3