diff options
Diffstat (limited to 'src/cmd_auth.c')
| -rw-r--r-- | src/cmd_auth.c | 67 |
1 files changed, 40 insertions, 27 deletions
diff --git a/src/cmd_auth.c b/src/cmd_auth.c index 86dab12..502057c 100644 --- a/src/cmd_auth.c +++ b/src/cmd_auth.c @@ -18,38 +18,33 @@ /* login rate limiting (in-memory, per key) */ /* ------------------------------------------------------------------ */ -#define RL_MAX_KEYS 16 +/* Keyed per user name ("u:<name>") and per user for password changes + ("pw:<id>"): wrong guesses lock the guessed account, never everybody. + A full table reuses an expired entry, else the least-failed one, so the + limiter never switches itself off. */ +#define RL_MAX_KEYS 1024 #define RL_MAX_FAILS 5 #define RL_WINDOW 900 struct rl_entry { - char key[64]; + char key[80]; int fails; int64_t window_end; }; static struct rl_entry g_rl[RL_MAX_KEYS]; -static struct rl_entry *rl_get(const char *key, int create) +static struct rl_entry *rl_find(const char *key) { - struct rl_entry *slot = NULL; - for (size_t i = 0; i < RL_MAX_KEYS; i++) { + for (size_t i = 0; i < RL_MAX_KEYS; i++) if (g_rl[i].key[0] && strcmp(g_rl[i].key, key) == 0) return &g_rl[i]; - if (create && !g_rl[i].key[0] && !slot) - slot = &g_rl[i]; - } - if (create && slot) { - snprintf(slot->key, sizeof slot->key, "%s", key); - slot->fails = 0; - slot->window_end = 0; - } - return slot; + return NULL; } static int rl_blocked(const char *key, int64_t *retry_after) { - struct rl_entry *e = rl_get(key, 0); + struct rl_entry *e = rl_find(key); if (!e || e->fails < RL_MAX_FAILS) return 0; int64_t now = util_now(); @@ -62,22 +57,38 @@ static int rl_blocked(const char *key, int64_t *retry_after) static void rl_fail(const char *key) { - struct rl_entry *e = rl_get(key, 1); - if (!e) - return; int64_t now = util_now(); - if (e->fails == 0 || e->window_end <= now) + struct rl_entry *e = rl_find(key); + if (!e) { + /* a free or expired slot, else the one with the fewest failures + (a blocked entry is replaced last, so flooding the table with + names does not lift a block) */ + e = &g_rl[0]; + for (size_t i = 0; i < RL_MAX_KEYS; i++) { + struct rl_entry *c = &g_rl[i]; + if (!c->key[0] || c->window_end <= now) { + e = c; + break; + } + if (c->fails < e->fails || + (c->fails == e->fails && c->window_end < e->window_end)) + e = c; + } + memset(e, 0, sizeof *e); + snprintf(e->key, sizeof e->key, "%s", key); + } + if (e->fails == 0 || e->window_end <= now) { + e->fails = 0; e->window_end = now + RL_WINDOW; + } e->fails++; } static void rl_ok(const char *key) { - struct rl_entry *e = rl_get(key, 0); - if (e) { - e->fails = 0; - e->window_end = 0; - } + struct rl_entry *e = rl_find(key); + if (e) + memset(e, 0, sizeof *e); } /* ------------------------------------------------------------------ */ @@ -188,7 +199,9 @@ static yyjson_mut_val *h_session_open(struct req *r) return fail(r, "INVALID_ARGS", "username and password are required"); } int64_t retry = 0; - if (rl_blocked("local", &retry)) { + char rlkey[80]; + snprintf(rlkey, sizeof rlkey, "u:%s", username); + if (rl_blocked(rlkey, &retry)) { free(reqjson); return failf(r, "RATE_LIMITED", "too many failed logins, retry in %lld seconds", @@ -202,14 +215,14 @@ static yyjson_mut_val *h_session_open(struct req *r) auth_verify_password(pwhash, password) == 0; free(pwhash); if (!ok) { - rl_fail("local"); + rl_fail(rlkey); audit_append(r->db, 0, uid > 0 ? uid : 0, 0, "auth.fail", reqjson, "AUTH_FAILED", NULL); free(display); free(reqjson); return fail(r, "AUTH_FAILED", "invalid credentials"); } - rl_ok("local"); + rl_ok(rlkey); int64_t active = 0; yyjson_mut_val *orgs = orgs_for_user(r->db, r->rdoc, uid, 0, &active); |
