Limit wrong passwords on the room browser

POST /api/room checked the room password on every request with no failure
limit, and a browser may send a new identity token each time. Any host that
reached WebConfig could guess the room admin password, which is also the
radio and /api/login admin password, at main-loop speed.

One budget now covers all browser tokens, with /api/login's numbers: at most
five distinct wrong passwords in each thirty-second window. After that, a
request carrying a password is refused without checking it until the window
ends. Wrong passwords count in read-only rooms too. Empty passwords are never
counted or refused, so public read-only access keeps working. The last four
wrong passwords are remembered by 8-byte SHA-256 digest and not counted
again, so a tab still polling with an old or mistyped password cannot use up
the budget. Successful sign-ins do not refund failures. Radio login is
unchanged.

test_room_web_service.py gains PASSWORD_LIMIT, which runs the real
processRoomRequest: refusal after five failures from five tokens; identical
replies for wrong, guest and admin passwords while refused, with no password
check (calls to authorizeRoomLogin are counted); no lock extension from 300
refused requests; the 29999/30000 ms boundary and the first window after
boot; read-only and empty-password cases; no refund on success; repeated,
rotating and read-only stale passwords; slow distinct failures ageing out;
and a guard that the digest ring stays in bounds. It fails on the previous
processRoomRequest at the sixth guess.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nullrouten0andClaude Opus 5.5 committed 2026-10-18 03:57:09 +00:00
1 parent 0e72474f41
commit a6f49e2b63
4 files changed
+162 -3

No files matched your search

+26 -1
View File
@@ -438,9 +438,35 @@ void MyMesh::processRoomRequest(const uint8_t* token, uint32_t sequence, char* r
mesh::Identity author;
mesh::Utils::sha256(author.pub_key, PUB_KEY_SIZE, token, 32, self_id.pub_key, PUB_KEY_SIZE);
if (!room_access.allowsIdentity(author.pub_key)) { error("room access denied"); return; }
// Every request carries the password and a browser may send a new token each
// time, so one budget covers all tokens, with /api/login's numbers: five
// wrong passwords per thirty seconds. An empty password tests nothing and
// keeps public read-only access.
auto& limit = room_web_password_limit;
const uint32_t now = millis();
const bool window_open = uint32_t(now - limit.window) < 30000UL;
if (password[0] && limit.failures >= 5 && window_open) {
error("too many wrong room passwords; wait 30 seconds"); return;
}
const auto authorization = mesh::authorizeRoomLogin(password, _prefs.password,
_prefs.guest_password, false, 0, _prefs.allow_read_only, PERM_ACL_ROLE_MASK,
PERM_ACL_GUEST, PERM_ACL_READ_WRITE, PERM_ACL_ADMIN);
// Read-only rooms admit a wrong password as a guest; it still failed. A
// password that already failed reveals nothing new, so a tab still polling
// with an old or mistyped one is counted once, not on every request.
if (password[0] && authorization.role == PERM_ACL_GUEST) {
uint8_t digest[8];
mesh::Utils::sha256(digest, sizeof(digest), (const uint8_t*)password, strlen(password));
digest[0] |= 1; // never matches an unused, all-zero slot
bool repeated = false;
for (const auto& recent : limit.recent) repeated |= memcmp(recent, digest, sizeof(digest)) == 0;
if (!repeated) {
memcpy(limit.recent[limit.recent_next], digest, sizeof(digest));
limit.recent_next = (limit.recent_next + 1) % 4;
if (!limit.failures || !window_open) { limit.window = now; limit.failures = 0; }
++limit.failures;
}
}
if (!authorization.accepted) { error("incorrect room password"); return; }
room_web_request_authenticated = true;
const uint8_t role = authorization.role & PERM_ACL_ROLE_MASK;
@@ -480,7 +506,6 @@ void MyMesh::processRoomRequest(const uint8_t* token, uint32_t sequence, char* r
error("unknown room operation"); return;
}
const uint32_t now = millis();
RoomWebUser* user = nullptr;
RoomWebUser* available = nullptr;
for (auto& entry : room_web_users) {