From 735482d3b6676f040228f5205d48a30a1a99512c Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sat, 3 Oct 2026 22:12:44 +0200 Subject: [PATCH] Pithos 0.3.3: fix SignatureDoesNotMatch when opening a bucket The SigV4 encoder kept '/' unescaped in object paths by swapping in a "never matches" regex (/$^/) for query values, but that regex matches the empty string, so every empty query value went out as "/": prefix= became prefix=/ and ?policy became ?policy=/. The request then no longer matched what was signed, and listing any bucket or reading its policy failed against s3d. Bucket listing and creation carry no query, so they worked. Verified end to end against s3d (list, folders, upload, download, public policy, folder delete) and against the aws cli; two regression tests. --- core/s3.js | 8 +++++--- package.json | 2 +- test/sigv4.test.js | 17 +++++++++++++++++ theseus/addon.json | 2 +- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/core/s3.js b/core/s3.js index 8acaeda..2153ab0 100644 --- a/core/s3.js +++ b/core/s3.js @@ -14,9 +14,11 @@ const hmac = (k, s) => crypto.createHmac('sha256', k).update(s).digest(); // RFC 3986 encoding as SigV4 wants it; '/' kept for object keys in paths. function uriEncode(str, keepSlash) { - return encodeURIComponent(str) - .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()) - .replace(keepSlash ? /%2F/g : /$^/, '/'); + const out = encodeURIComponent(str) + .replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()); + // (A "never matches" regex like /$^/ is not one: it matches the empty + // string, which turned every empty query value into "/".) + return keepSlash ? out.replace(/%2F/g, '/') : out; } function canonicalQuery(query) { diff --git a/package.json b/package.json index 9ebe0eb..abef19e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pithos", - "version": "0.3.2", + "version": "0.3.3", "description": "A control panel for s3d, the Sia S3 gateway: daemon, users and keys, buckets, upload pipeline.", "license": "MPL-2.0", "type": "module", diff --git a/test/sigv4.test.js b/test/sigv4.test.js index cc4f88b..cf99345 100644 --- a/test/sigv4.test.js +++ b/test/sigv4.test.js @@ -77,3 +77,20 @@ test('presigned URL matches aws cli', { skip: !hasAws && 'aws cli not installed' assert.equal(ours.pathname, t.pathname); assert.equal(ours.searchParams.get('X-Amz-Signature'), t.searchParams.get('X-Amz-Signature')); }); + +// Regression: empty query values (prefix=, policy) were sent as "/" and +// every bucket listing failed with SignatureDoesNotMatch against s3d. +test('empty query values stay empty and match the aws cli', { skip: !hasAws && 'aws cli not installed' }, async () => { + let port; + const req = await capture((p) => { port = p; return runAws(['s3api', 'list-objects-v2', '--bucket', 'my-bucket', '--prefix', '', '--delimiter', '/', '--endpoint-url', `http://127.0.0.1:${p}`]); }); + const { theirs, ours } = resign(port, req, 'my-bucket'); + assert.equal(ours, theirs); +}); + +test('client sends empty values as empty', async () => { + const seen = await new Promise((resolve) => { + const server = http.createServer((req, res) => { res.end('false'); server.close(); resolve(req.url); }); + server.listen(0, '127.0.0.1', () => new S3Client({ endpoint: `http://127.0.0.1:${server.address().port}`, accessKeyId: AK, secretKey: SK }).listObjects('b').catch(() => {})); + }); + assert.match(seen, /[?&]prefix=(&|$)/); +}); diff --git a/theseus/addon.json b/theseus/addon.json index 34c9504..8ea5cc9 100644 --- a/theseus/addon.json +++ b/theseus/addon.json @@ -1,7 +1,7 @@ { "id": "pithos", "name": "Pithos", - "version": "0.3.2", + "version": "0.3.3", "description": "Run s3d, the Sia S3 gateway, from the Theseus sidebar: connect it to a Sia indexer, create S3 users and access keys, browse and share buckets, and watch uploads reach Sia.", "author": "Silent Mode", "icon": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJnIiB4MT0iMCIgeTE9IjAiIHgyPSIwIiB5Mj0iMSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjM2RkYzk3Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMWU4ZjZhIi8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHBhdGggZD0iTTIyIDZoMjB2NWMwIDItMiAzLTIgNSA4IDMgMTQgMTEgMTQgMjEgMCAxMi05IDIxLTIyIDIxUzEwIDQ5IDEwIDM3YzAtMTAgNi0xOCAxNC0yMSAwLTItMi0zLTItNXoiIGZpbGw9InVybCgjZykiLz48cGF0aCBkPSJNMTQgMzBoMzZNMTMgNDBoMzgiIHN0cm9rZT0iIzBiMWYxOCIgc3Ryb2tlLXdpZHRoPSIzIiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9Ii41NSIvPjwvc3ZnPgo=",