Skip to content

Commit 7917a77

Browse files
committed
Throttle email updates per wallet
1 parent f047d24 commit 7917a77

2 files changed

Lines changed: 17 additions & 5 deletions

File tree

lib/api/account.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ module.exports = function registerAccountRoutes(ctx) {
3535
return "";
3636
}
3737

38-
function sensitiveKey(kind, username, from, to, enabled) {
38+
function sensitiveKey(kind, username) {
3939
const userValue = sensitivePart(username);
4040
if (!userValue) return null;
4141

4242
const hash = crypto.createHash("sha256");
43-
for (const part of [kind, userValue, sensitivePart(from), sensitivePart(to), sensitivePart(enabled)]) {
43+
for (const part of [sensitivePart(kind), userValue]) {
4444
hash.update(String(Buffer.byteLength(part)));
4545
hash.update(":");
4646
hash.update(part);
@@ -140,7 +140,7 @@ module.exports = function registerAccountRoutes(ctx) {
140140
if (body) return sendJson(res, 401, { success: false, msg: "No \"username\" parameter was found" });
141141
return;
142142
}
143-
const throttleKey = sensitiveKey("subscribeEmail", body.username, body.from, body.to, body.enabled);
143+
const throttleKey = sensitiveKey("subscribeEmail", body.username);
144144
const throttle = acquireSensitiveAttempt(throttleKey);
145145
if (throttle) return sendJson(res, 429, throttle);
146146

tests/api/public_and_auth.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -347,7 +347,8 @@ test.describe("api public and auth", { concurrency: false }, () => {
347347
});
348348
});
349349

350-
test("email subscription throttle does not let a mismatched email block a valid update", async () => {
350+
test("email subscription throttle cannot be bypassed by changing email fields", async () => {
351+
let nowValue = 0;
351352
let updateCalls = 0;
352353
const mysql = createMysql(async function handler(sql, params) {
353354
if (sql.startsWith("UPDATE users SET enable_email = ?, email = ? WHERE username = ? AND email = ?")) {
@@ -365,7 +366,7 @@ test.describe("api public and auth", { concurrency: false }, () => {
365366
config: createConfig(),
366367
database: createDatabase({ caches: {} }),
367368
mysql: mysql,
368-
now: () => 0,
369+
now: () => nowValue,
369370
support: createSupport()
370371
}, async (port) => {
371372
const attackerFailure = await requestJson(port, "POST", "/user/subscribeEmail", {
@@ -377,6 +378,17 @@ test.describe("api public and auth", { concurrency: false }, () => {
377378
assert.equal(attackerFailure.statusCode, 401);
378379
assert.deepEqual(attackerFailure.json, { error: "FROM email does not match" });
379380

381+
const variedGuess = await requestJson(port, "POST", "/user/subscribeEmail", {
382+
username: "wallet",
383+
enabled: 0,
384+
from: "old@example.com",
385+
to: "new@example.com"
386+
});
387+
assert.equal(variedGuess.statusCode, 429);
388+
assert.match(variedGuess.json.msg, /Too many attempts/);
389+
assert.equal(updateCalls, 1);
390+
391+
nowValue += 2001;
380392
const validUpdate = await requestJson(port, "POST", "/user/subscribeEmail", {
381393
username: "wallet",
382394
enabled: 1,

0 commit comments

Comments
 (0)