From d99edbddfaf894db3586bc9032cfe561caa785cf Mon Sep 17 00:00:00 2001 From: Bryce Kwon Date: Sat, 7 Nov 2026 14:31:08 -1000 Subject: Check every failed login answers and logs alike Every post carried both fields, so the guards for a missing name or password never ran. An unknown name was checked for the cookie and the Location alone, so an extra header or body for it passed, and only the wrong password case had its log line checked for the name and against the password. --- tests/extensions/test-auth.lua | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/extensions/test-auth.lua b/tests/extensions/test-auth.lua index dd719d9..d6982a4 100644 --- a/tests/extensions/test-auth.lua +++ b/tests/extensions/test-auth.lua @@ -544,6 +544,7 @@ constant_equals = real_constant_equals h.equals("and logs nothing", h.error_log(), "") out = login("Alice", "wrong", way_back) +local wrong_password = out h.contains("a bad password redirects the same way", out, "Status: 302") h.contains("back to the page with the failure flagged", out, "Location: /repo/?login=failed\n") h.contains("but clears the session cookie", out, "Set-Cookie: " .. session_name .. "=; ") @@ -557,10 +558,31 @@ h.contains("a name cannot forge a log line of its own", h.error_log(), -- A failed login for a name with no account spends crypt on a configured -- hash, the same work a real check costs, so timing tells nothing. crypt_settings = {} -out = login("nobody", "x", way_back) -h.contains("an unknown user is told nothing different", out, "Set-Cookie: " .. session_name .. "=; ") -h.contains("and lands on the same flagged page", out, "Location: /repo/?login=failed\n") +out = login("nobody", "unknown secret", way_back) +h.equals("an unknown user is told nothing different", out, wrong_password) h.equals("a missing account spends crypt on a configured hash", crypt_settings[1], stored_hash) +h.contains("and is logged by name", h.error_log(), "[cgit] Failed login as nobody for /repo/\n") +h.excludes("but never with the password", h.error_log(), "unknown secret") + +-- A post missing a field, or with a field that has no value, is a failed +-- login like any other rather than an error. +local back = url_encode(way_back) +for _, case in ipairs({ + { "with no name", "password=hunter2&redirect=" .. back, "" }, + { "with no password", "username=Alice&redirect=" .. back, "Alice" }, + { "with neither", "redirect=" .. back, "" }, + { "with a field that has no value", "hunter2&redirect=" .. back, "" }, +}) do + local answered, answer = pcall(run_action, "authenticate-post", { method = "POST", body = case[2] }) + h.check("a login " .. case[1] .. " is answered without an error", answered, tostring(answer)) + h.contains("a login " .. case[1] .. " clears the session cookie", tostring(answer), + "Set-Cookie: " .. session_name .. "=; ") + h.contains("a login " .. case[1] .. " lands on the flagged page", tostring(answer), + "Location: /repo/?login=failed\n") + h.contains("a login " .. case[1] .. " is logged", h.error_log(), + "[cgit] Failed login as " .. case[3] .. " for /repo/\n") + h.excludes("a login " .. case[1] .. " never logs a password", h.error_log(), "hunter2") +end if variant == "file" then h.redirect_file("/etc/cgit-auth/users", directory .. "/auth-users-missing") @@ -583,6 +605,8 @@ crypt_settings = {} out = login("Alice", string.rep("a", 257), way_back) h.contains("an overlong password is turned down", out, "Set-Cookie: " .. session_name .. "=; ") h.equals("without spending crypt on it", #crypt_settings, 0) +h.contains("and is logged like any failed login", h.error_log(), "[cgit] Failed login as Alice for /repo/\n") +h.excludes("without the password", h.error_log(), string.rep("a", 257)) login("Alice", string.rep("a", 256), way_back) h.equals("a password at the cap is still checked", #crypt_settings, 1) -- cgit 2.12.1