aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-29 08:37:39 -0700
committerAdam Soltys <[email protected]>2026-09-29 08:37:47 -0700
commite96ad1cea4192a608a7f15896e4b690b57bf8fc9 (patch)
treefa6af41a1583e3830c03c907fcd02b54b024aab6
parentb1e175c1faa46ed67cf0575f8d8808f1ae10892c (diff)
downloadcoinos-server-e96ad1cea4192a608a7f15896e4b690b57bf8fc9.tar.gz
coinos-server-e96ad1cea4192a608a7f15896e4b690b57bf8fc9.zip
invoices: don't let a supplied id overwrite another user's invoice
generate() writes invoice:<id> wholesale with the caller's uid, and the old bolt11 pointer keeps resolving to that id, so passing someone else's invoice id re-pointed their invoice at the caller's account. Invoice ids aren't secret (LUD-21 verify URLs, invoice pages, receipts), so refuse an existing id that belongs to a different user, both in generate() and in POST /invoice for anonymous pay-page callers. Both fail with "invoice not found" so ids can't be probed for existence. Co-Authored-By: Claude Fable 5.1 <[email protected]>
-rw-r--r--lib/invoices.ts11
-rw-r--r--routes/invoices.ts10
2 files changed, 21 insertions, 0 deletions
diff --git a/lib/invoices.ts b/lib/invoices.ts
index ba55b77c..7f25f4ba 100644
--- a/lib/invoices.ts
+++ b/lib/invoices.ts
@@ -84,6 +84,17 @@ export const generate = async ({ invoice, user }) => {
if (memo && memo.length > 5000) fail("memo too long");
if (!id) id = v4();
+ else {
+ // A caller-supplied id re-uses an existing record (tip/webhook update,
+ // repeated bolt12 offer create). It must be the caller's own: the write
+ // below replaces `invoice:<id>` wholesale with uid = this user, and the
+ // old bolt11 pointer keeps resolving to the same id, so overwriting
+ // another user's invoice re-routes whatever their payer sends into this
+ // account. Same error as a missing invoice so ids can't be probed for
+ // existence.
+ const existing = await getInvoice(id);
+ if (existing && existing.uid !== user.id) fail("invoice not found");
+ }
let hash;
let text;
diff --git a/routes/invoices.ts b/routes/invoices.ts
index 9ba45af6..b8d06bba 100644
--- a/routes/invoices.ts
+++ b/routes/invoices.ts
@@ -36,6 +36,16 @@ export default {
else invoice.own = false;
try {
+ // A supplied id that already exists is an UPDATE of that record —
+ // generate() rewrites it wholesale. Only the authenticated owner may do
+ // that. Without this, an anonymous pay-page session passing
+ // body.user = merchant could rewrite the amount/memo of a merchant's
+ // live invoice. generate() separately refuses to move an invoice to a
+ // different uid.
+ if (invoice?.id) {
+ const existing = await getInvoice(invoice.id);
+ if (existing && existing.uid !== req.user.id) fail("invoice not found");
+ }
const result = await generate({ invoice, user });
res.send(result);
} catch (e) {