Skip to content

Commit 7832b60

Browse files
authored
Merge pull request #838 from davedumto/audit/path-payment-service-598-600-601
audit(path-payment): security + query audits + metrics fallback fix (#598, #600, #601)
2 parents 95e9703 + bec9e59 commit 7832b60

4 files changed

Lines changed: 136 additions & 2 deletions

File tree

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Performance Audit — Path Payment Service SQL queries (issue #601)
2+
3+
## Scope & method
4+
5+
Review every DB query in the Path Payment Service flow (`src/routes/payments.js`
6+
path-payment-quote handler + `src/services/paymentService.js`) for N+1 access,
7+
unbounded scans, missing filters/indexes, and avoidable work.
8+
9+
## Findings
10+
11+
| Query | Location | Assessment |
12+
| --- | --- | --- |
13+
| Path-payment quote lookup | `payments.js:1148` | Single-row `maybeSingle()` by **primary key** `id`, scoped by `merchant_id` (when present) and `deleted_at IS NULL`, selecting only the 6 needed columns. **Already optimal** — a PK lookup, no scan. |
14+
| Rolling 7-day metrics (primary) | `paymentService.js` (`rolling-payment-metrics` SQL) | **Aggregated in the database**: a CTE chain with `date_trunc('day', created_at)`, `GROUP BY`, `COUNT(*) FILTER (...)`, and a `totals` CTE — one round trip, no per-row app work. Already optimal. |
15+
| Rolling 7-day metrics (Supabase fallback) | `paymentService.js:getRollingMetricsViaSupabase` | Fetch-all-and-reduce fallback for Supabase-only deployments. Filters by `merchant_id`, `deleted_at`, and a 7-day `created_at` window. **One avoidable cost:** it requested `ORDER BY created_at` even though results are bucketed into a `Map` and read back via a fixed 7-day loop (order-independent). |
16+
| Payments list | `paymentService.js:290` | Column-scoped `select`, `merchant_id` filter, paginated. Fine. |
17+
18+
No N+1 patterns and no unbounded `SELECT *` scans were found in the path-payment
19+
flow. The path-finding step calls Horizon (`findStrictReceivePaths`), not SQL.
20+
21+
## Change applied
22+
23+
Removed the redundant `.order("created_at", { ascending: true })` from the
24+
Supabase metrics fallback (`getRollingMetricsViaSupabase`). The result is
25+
independent of row order (bucketed by day, then read through a fixed 7-day
26+
array), so the sort was pure overhead on a potentially large 7-day window.
27+
28+
## Conclusion
29+
30+
The Path Payment Service's queries were already well-optimized (PK lookups +
31+
in-database `date_trunc` aggregation). The single genuine, safe improvement —
32+
dropping an unnecessary sort in the fallback path — is included in this PR. No
33+
schema/index changes are warranted.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Security Audit — Path Payment Service signature verification (issue #600)
2+
3+
## Scope & method
4+
5+
Review whether the Path Payment Service performs cryptographic signature
6+
verification, where it is enforced, and whether the implementation is sound.
7+
8+
## Finding: full Ed25519 signature verification already exists and is enforced
9+
10+
`verifyTransactionSignature(txHash)` in `src/lib/stellar.js:731` performs
11+
**complete** cryptographic verification, not a presence check:
12+
13+
1. Fetches the transaction and deserialises its XDR envelope.
14+
2. Rejects envelopes with **no signatures** (`stellar.js:782`).
15+
3. Loads the source account to read its signers and **medium threshold**.
16+
4. For every decorated signature, derives the signer, performs a real
17+
**Ed25519 `keyPair.verify(txHashBytes, sigBytes)`** (`stellar.js:846`), and
18+
accumulates signing weight.
19+
5. Tracks `usedSigners` to **prevent signature replay** (`stellar.js:828`).
20+
6. Accepts only when accumulated weight meets the account's medium threshold.
21+
22+
It is wired into the payment-verification flow via
23+
`verifyTransactionSignatureIfAvailable(...)`:
24+
25+
- `src/routes/payments.js:601` — verification endpoint checks the matched
26+
transaction's signature and rejects via `isSignatureVerificationAccepted(...)`.
27+
- `src/services/paymentService.js:107` / `verifyPayment(...)` (`paymentService.js:618`).
28+
29+
Existing test coverage: `src/lib/signature-verification.test.js`,
30+
`src/lib/transaction-signer.test.js`, `src/routes/payments-security.test.js`.
31+
32+
## Why the read-only quote endpoint is intentionally unsigned
33+
34+
`GET /api/path-payment-quote/:id` (`src/routes/payments.js:1137`) is **public by
35+
design**: the customer-facing payment page calls it with no API key
36+
(`frontend/src/app/pay/[id]/page.tsx:304`) because the payer needs the quote
37+
(amount/asset/path) to construct their own transaction. It performs no state
38+
change and exposes only data already inherent to the public payment link, so it
39+
is correctly **not** gated by signature/API-key auth. Adding mandatory
40+
verification here would break checkout without a security benefit.
41+
42+
## Conclusion
43+
44+
The cryptographic signature verification this issue asks for is **already
45+
implemented, enforced on the state-changing path, and tested**. No code change
46+
was warranted; this audit documents the coverage and the deliberate public-read
47+
exception.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Security Audit — Trustline Manager (issue #598)
2+
3+
## Scope & method
4+
5+
Static review of the entire `backend/` tree for any trustline-management surface:
6+
`changeTrust` / `change_trust` / `allowTrust` operations, `Asset` trustline
7+
handling, and any module named "Trustline Manager".
8+
9+
## Finding: no Trustline Manager module exists
10+
11+
There is **no trustline-management code in the repository**:
12+
13+
- `grep -rniE "changeTrust|change_trust|allowTrust|trustline"` over `backend/src`
14+
returns **zero** matches.
15+
- The only Stellar asset/operation surface is in `src/lib/stellar.js`:
16+
- `StellarSdk.Asset.native()` (native XLM only) — `stellar.js:237`
17+
- `StellarSdk.Operation.payment(...)``stellar.js:638`
18+
- `findStrictReceivePaths(...)` (strict-receive path **quoting**, read-only) — `stellar.js:418`
19+
- `verifyTransactionSignature(...)` (signature verification) — `stellar.js:731`
20+
21+
The platform settles payments against assets the receiving account already
22+
trusts; it never builds, submits, or manages `changeTrust` operations on behalf
23+
of users. So there is no Trustline Manager to audit.
24+
25+
## Security implication of the current design
26+
27+
Because the backend never issues `changeTrust`, the trustline attack surface
28+
(unbounded trust limits, trusting an attacker-controlled issuer, trustline
29+
removal griefing) **does not exist server-side** — trust decisions remain with
30+
the end user's wallet. This is the safer default.
31+
32+
## Recommendations (for if/when a Trustline Manager is introduced)
33+
34+
If trustline management is added later, the audit checklist should be:
35+
36+
1. **Issuer allow-listing** — never `changeTrust` to an arbitrary
37+
caller-supplied issuer; validate against a vetted asset registry.
38+
2. **Explicit trust limits** — set a bounded `limit` rather than the max default.
39+
3. **Authorization** — gate any trustline mutation behind `requireApiKeyAuth`
40+
and merchant scoping (the pattern already used for `/api/payments*` in
41+
`src/app.js:282`).
42+
4. **Signature verification** — reuse `verifyTransactionSignature`
43+
(`src/lib/stellar.js:731`) before submitting any user-authorized changeTrust.
44+
5. **Idempotency** — route mutations through `idempotencyMiddleware`.
45+
46+
## Conclusion
47+
48+
No action possible against a non-existent module; documented the absence, the
49+
(safe) reason for it, and the audit criteria for a future implementation. This
50+
issue should be re-scoped to "build a Trustline Manager" before code work is
51+
meaningful.

backend/src/services/paymentService.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -397,13 +397,16 @@ async function getRollingMetricsViaSupabase(merchantId) {
397397
const sevenDaysAgo = new Date();
398398
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
399399

400+
// No ORDER BY: the rows are bucketed by day into `metricsMap` and then read
401+
// back through a fixed 7-day loop, so the result is independent of row order.
402+
// Dropping the sort avoids an unnecessary ordering step on what can be a large
403+
// 7-day window of a busy merchant's payments (issue #601).
400404
const { data: payments, error } = await supabase
401405
.from("payments")
402406
.select("amount, created_at, status")
403407
.eq("merchant_id", merchantId)
404408
.is("deleted_at", null)
405-
.gte("created_at", sevenDaysAgo.toISOString())
406-
.order("created_at", { ascending: true });
409+
.gte("created_at", sevenDaysAgo.toISOString());
407410

408411
if (error) {
409412
error.status = 500;

0 commit comments

Comments
 (0)