Skip to content

Commit 09fed94

Browse files
review pass-2 fix (#573): hash-check payload for falsy-parsed bodies too
Copilot pass-2 (separate from the Buffer fix): the `if (payloadTag && request.body)` guard skipped payload validation when a JSON body parsed to a falsy value (null/false/0/""), so a client could sign a payload tag, send a falsy body, and the integrity check silently didn't run. Guard on `request.body !== undefined` instead — Fastify leaves body undefined only when no body was sent — so falsy-but-present bodies are still verified while genuinely bodyless requests stay skipped. New test: a `false` body with a WRONG payload tag is now rejected (was skipped), and with the correct tag passes. 8/8 in file; full suite green.
1 parent 3dda984 commit 09fed94

2 files changed

Lines changed: 23 additions & 1 deletion

File tree

src/auth/nostr.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,12 @@ export async function verifyNostrAuth(request) {
243243

244244
// Validate payload hash if present and request has body
245245
const payloadTag = getTagValue(event, 'payload');
246-
if (payloadTag && request.body) {
246+
// Validate whenever a payload tag is present AND a body was provided —
247+
// keyed on `!== undefined`, not truthiness, so a JSON body that parses
248+
// to a falsy value (`null`, `false`, `0`, `""`) is still hash-checked
249+
// rather than silently skipping the integrity guard (Copilot review on
250+
// #573). Fastify leaves request.body `undefined` when no body was sent.
251+
if (payloadTag && request.body !== undefined) {
247252
// Hash the EXACT bytes the client signed. NIP-98's `payload` tag is
248253
// sha256(request body) over the wire bytes. crypto.update() accepts a
249254
// string (encoded UTF-8) or a Buffer (raw bytes), so we pass each

test/nip98-payload-hash.test.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ describe('NIP-98 payload hash uses the raw request bytes (#565)', () => {
134134
`compact body should still pass; got error: ${result.error}`);
135135
});
136136

137+
it('a falsy JSON body (false) is still hash-checked, not skipped (Copilot review on #573)', async () => {
138+
// The body literally parses to `false`. A WRONG payload tag must still
139+
// be caught — under the old `&& request.body` truthiness guard this
140+
// validation was skipped entirely for falsy bodies.
141+
const falsyRaw = 'false';
142+
const wrongToken = nip98Token(url, 'PUT', sk, 'a-different-body');
143+
const mismatch = await verifyNostrAuth(mockRequest(wrongToken, { rawBody: falsyRaw, body: false }));
144+
assert.strictEqual(mismatch.error, 'Payload hash mismatch',
145+
'a falsy body with a wrong payload tag must be rejected, not skipped');
146+
147+
// And the matching payload for the same falsy body passes.
148+
const goodToken = nip98Token(url, 'PUT', sk, falsyRaw);
149+
const ok = await verifyNostrAuth(mockRequest(goodToken, { rawBody: falsyRaw, body: false }));
150+
assert.notStrictEqual(ok.error, 'Payload hash mismatch',
151+
`a falsy body with the correct payload should pass; got ${ok.error}`);
152+
});
153+
137154
it('a binary / non-UTF-8 Buffer body hashes the raw bytes (Copilot review on #573)', async () => {
138155
// Bytes that are NOT valid UTF-8 — a .toString() round-trip would
139156
// mangle them (replacement chars) and break the hash. The client

0 commit comments

Comments
 (0)