mirror of
https://github.com/kevinveenbirkenbach/homepage.veen.world.git
synced 2026-08-24 05:04:33 +00:00
fix(app): stop trusting X-Forwarded-For, and pin what the audit found
ProxyFix defaults x_for to 1, so ProxyFix(app.wsgi_app, x_proto=1) never disabled it: request.remote_addr and the access log were forgeable by any client that reached the app directly. It is x_for=0 now, asserted rather than assumed. A mutation audit over the change set reverted 196 deliberate behaviours and found 47 that no test noticed. This closes the ones that carry damage: - apod_background lost its key check, its transport guard, its status guard and its media-type check without a single test failing. Each one turns a slow or unhappy NASA into a 500 on every page. - Untrusted values reached innerHTML through window.I18N, which the translation backend writes, and the modal's click handlers stacked so a later click opened an earlier popup's URL. - The sync tool could ask for HTML instead of text, translate from "auto" instead of English, run without a timeout, store an empty translation that marks the string done for good, abandon 28 languages because one could not be written, and report success after reaching nothing. - Neither the lint target, the CI jobs, the vendored RTL stylesheet, the documented environment keys, nor any of the four hardenings in scripts/run-e2e.sh was observed by anything. Three of the new tests passed for the wrong reason on their first cut — a mock that answered None whether or not the guard existed, a raise_for_status that was never called, a string that stayed in the file after the mutation. The audit found those too; all 24 reverts now fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,11 @@ class TestCollectSources(unittest.TestCase):
|
||||
def test_blank_values_are_ignored(self):
|
||||
self.assertEqual(i18n_sync.collect_sources({"text": " "}), set())
|
||||
|
||||
def test_a_list_of_prose_is_collected(self):
|
||||
self.assertEqual(
|
||||
i18n_sync.collect_sources({"text": ["one", "two"]}), {"one", "two"}
|
||||
)
|
||||
|
||||
|
||||
class TestTranslate(unittest.TestCase):
|
||||
def _session(self, ok, payload=None, status=200, text=""):
|
||||
@@ -79,7 +84,33 @@ class TestTranslate(unittest.TestCase):
|
||||
self.assertNotIn("api_key", session.post.call_args.kwargs["data"])
|
||||
|
||||
def test_failed_response_returns_none(self):
|
||||
session = self._session(False, status=403, text="denied")
|
||||
session = self._session(
|
||||
False, {"translatedText": "SHOULD NOT BE USED"}, status=403, text="denied"
|
||||
)
|
||||
|
||||
self.assertIsNone(i18n_sync.translate(session, "http://lt", "", "Hi", "de"))
|
||||
|
||||
def test_the_request_asks_for_plain_text_from_the_source_language(self):
|
||||
session = self._session(True, {"translatedText": "Hallo"})
|
||||
|
||||
i18n_sync.translate(session, "http://lt", "", "Hello", "de")
|
||||
|
||||
data = session.post.call_args.kwargs["data"]
|
||||
self.assertEqual(data["format"], "text")
|
||||
self.assertEqual(data["source"], i18n.SOURCE_LANGUAGE)
|
||||
self.assertEqual(data["target"], "de")
|
||||
|
||||
def test_the_request_is_bounded_by_a_timeout(self):
|
||||
session = self._session(True, {"translatedText": "Hallo"})
|
||||
|
||||
i18n_sync.translate(session, "http://lt", "", "Hello", "de")
|
||||
|
||||
timeout = session.post.call_args.kwargs["timeout"]
|
||||
self.assertIsInstance(timeout, (int, float))
|
||||
self.assertGreater(timeout, 0)
|
||||
|
||||
def test_an_empty_translation_is_refused(self):
|
||||
session = self._session(True, {"translatedText": ""})
|
||||
|
||||
self.assertIsNone(i18n_sync.translate(session, "http://lt", "", "Hi", "de"))
|
||||
|
||||
@@ -125,6 +156,15 @@ class TestSupportedLanguages(unittest.TestCase):
|
||||
with self.assertRaises(i18n_sync.BackendError):
|
||||
i18n_sync.supported_languages("http://lt", session)
|
||||
|
||||
def test_an_error_status_is_reported(self):
|
||||
response = Mock()
|
||||
response.raise_for_status.side_effect = i18n_sync.requests.HTTPError("503")
|
||||
response.json.return_value = [{"code": "de"}]
|
||||
session = Mock(get=Mock(return_value=response))
|
||||
|
||||
with self.assertRaises(i18n_sync.BackendError):
|
||||
i18n_sync.supported_languages("http://lt", session)
|
||||
|
||||
def test_a_non_json_listing_is_reported(self):
|
||||
session = self._session(side_effect=ValueError("no json"))
|
||||
|
||||
@@ -186,11 +226,11 @@ class TestWriteCatalog(unittest.TestCase):
|
||||
self.path = self.directory / "de.yaml"
|
||||
|
||||
def test_the_catalog_is_written_and_no_scratch_file_is_left(self):
|
||||
i18n_sync.write_catalog(self.path, {"Hi": "Hallo"})
|
||||
i18n_sync.write_catalog(self.path, {"Hi": "Hallo", "Umlaut": "Grüße"})
|
||||
|
||||
self.assertEqual(
|
||||
yaml.safe_load(self.path.read_text(encoding="utf-8")), {"Hi": "Hallo"}
|
||||
)
|
||||
text = self.path.read_text(encoding="utf-8")
|
||||
self.assertEqual(yaml.safe_load(text), {"Hi": "Hallo", "Umlaut": "Grüße"})
|
||||
self.assertIn("Grüße", text)
|
||||
self.assertEqual([p.name for p in self.directory.iterdir()], ["de.yaml"])
|
||||
|
||||
def test_a_write_that_dies_halfway_leaves_the_previous_catalog_intact(self):
|
||||
@@ -259,6 +299,11 @@ class TestSync(unittest.TestCase):
|
||||
self.assertFalse(self.path.exists())
|
||||
session.post.assert_not_called()
|
||||
|
||||
def test_an_empty_translation_is_not_stored(self):
|
||||
self._run(["Hello"], translated="")
|
||||
|
||||
self.assertFalse(self.path.exists())
|
||||
|
||||
def test_a_run_that_translated_nothing_leaves_the_file_untouched(self):
|
||||
original = "# Reviewed by a native speaker, keep the order.\nZebra: Zebra\n"
|
||||
self.path.write_text(original, encoding="utf-8")
|
||||
@@ -279,6 +324,25 @@ class TestSync(unittest.TestCase):
|
||||
self.assertEqual(session.post.call_count, 1)
|
||||
self.assertNotIn("Close", yaml.safe_load(self.path.read_text(encoding="utf-8")))
|
||||
|
||||
def test_a_language_that_cannot_be_written_does_not_stop_the_others(self):
|
||||
listing = Mock(raise_for_status=Mock())
|
||||
listing.json.return_value = [{"code": "de"}, {"code": "fr"}]
|
||||
answer = Mock(ok=True)
|
||||
answer.json.return_value = {"translatedText": "UEBERSETZT"}
|
||||
session = Mock(get=Mock(return_value=listing), post=Mock(return_value=answer))
|
||||
original = i18n_sync.write_catalog
|
||||
|
||||
def fails_for_german(path, catalog):
|
||||
if path.name == "de.yaml":
|
||||
raise OSError("read-only")
|
||||
original(path, catalog)
|
||||
|
||||
with patch.object(i18n_sync, "write_catalog", fails_for_german):
|
||||
with patch.object(i18n_sync.requests, "Session", return_value=session):
|
||||
i18n_sync.sync("http://lt", "", {"Hello"}, ["de", "fr"], self.directory)
|
||||
|
||||
self.assertTrue((self.directory / "fr.yaml").is_file())
|
||||
|
||||
def test_the_catalog_directory_is_created(self):
|
||||
nested = self.directory / "content"
|
||||
self.directory = nested
|
||||
|
||||
Reference in New Issue
Block a user