diff --git a/logs/audit.py b/logs/audit.py index d000be00..2d8b0b91 100644 --- a/logs/audit.py +++ b/logs/audit.py @@ -24,7 +24,7 @@ def write(user, host, instance, message, success=None, ip=None): except Exception as e: try: line = f"audit row not saved ({e!r}): {user} {host} {instance} success={success} ip={ip} {message}" - logger.warning(line.replace("\n", "\\n")) + logger.warning(line.replace("\r", "\\r").replace("\n", "\\n")) except Exception: pass diff --git a/logs/tests/test_login_rows.py b/logs/tests/test_login_rows.py index 556af810..7958c387 100644 --- a/logs/tests/test_login_rows.py +++ b/logs/tests/test_login_rows.py @@ -49,3 +49,34 @@ class LoginRowsTests(TestCase): self.assertEqual(self.login("right-pass-1").status_code, 429) self.assertEqual([r[2] for r in rows()][4:], ["Locked out", "Login failed"]) self.assertTrue(all(r[0] == "bob" and r[3] is False for r in rows())) + + +@override_settings(OTP_ENABLED=False) +class LoginRowInputTests(TestCase): + """S-16: what a failed login writes cannot break the row or forge another record.""" + + def login(self, username): + return self.client.post(reverse("accounts:login"), {"username": username, "password": "wrong"}) + + def test_an_oversized_username_is_cut_to_the_field(self): + self.assertEqual(self.login("x" * 300).status_code, 200) + self.assertEqual(rows(), [("x" * 150, "-", "Login failed", False, "127.0.0.1")]) + + def test_a_crlf_username_stays_in_its_row(self): + forged = "bob\r\nadmin - Login" + self.login(forged) + self.assertEqual(rows(), [(forged, "-", "Login failed", False, "127.0.0.1")]) + admin = get_user_model().objects.create_superuser("s16-admin", "s16@example.com", "pw") + self.client.force_login(admin) + page = self.client.get(reverse("admin:logs")).content.decode() + self.assertEqual(page.count("Login failed"), 1) + + def test_a_crlf_username_is_one_line_in_the_file_log(self): + from unittest.mock import patch + + with patch.object(Logs.objects, "create", side_effect=RuntimeError("db down")), \ + self.assertLogs("logs.audit", "WARNING") as logged: + self.login("bob\r\nforged record") + [line] = logged.output + self.assertNotIn("\r", line) + self.assertNotIn("\n", line)