From 72dcb04fd4dadc70a0a5ff0b0619865c0c4e37a7 Mon Sep 17 00:00:00 2001 From: jordan inskeep Date: Thu, 24 Sep 2026 08:18:32 -0400 Subject: [PATCH] Stop logging the Census API key in geoinfo_from_params The request parameters were logged at INFO after the key was added, so every geoinfo request wrote the key into notebook outputs. Log them before adding the key. Part of #7. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++++ morpc_census/geos.py | 3 ++- tests/test_geos_hierarchical.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3148bc..baa9ec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Security + +- **The Census API key is no longer written to logs.** `geoinfo_from_params()` logged its request parameters at INFO after adding `key`, so the key appeared in notebook outputs (hundreds of times per call through the hierarchical geography lookup). It now logs before adding the key. Keys passed on to `morpc.req` are redacted there from morpc 0.7.5 (morpc/morpc-py#207). + ## [0.6.4] — 2026-09-23 ### Fixed diff --git a/morpc_census/geos.py b/morpc_census/geos.py index 0593342..a922412 100644 --- a/morpc_census/geos.py +++ b/morpc_census/geos.py @@ -486,9 +486,10 @@ def geoinfo_from_params(param_dict: dict, year: int = 2024, output: Literal['lis if 'in' in param_dict: params.update({'in': param_dict['in']}) + # Log before adding the API key, so the key never reaches logs or notebook outputs. + logger.info(f"Getting GEOIDS from {url} and params: {params}.") if k := _get_api_key(): params['key'] = k - logger.info(f"Getting GEOIDS from {url} and params: {params}.") json = morpc.req.get_json_safely(url, params=params) if output == 'list': diff --git a/tests/test_geos_hierarchical.py b/tests/test_geos_hierarchical.py index bde2a15..92f1328 100644 --- a/tests/test_geos_hierarchical.py +++ b/tests/test_geos_hierarchical.py @@ -111,3 +111,16 @@ def test_county_subdivision_parts_pair_each_county_with_its_own_subdivisions(): ("county subdivision:18000", "county:049", "state:39"), ] assert len(result) == 3 + + +def test_geoinfo_from_params_does_not_log_the_api_key(caplog): + # The key is sent with the request but must never reach the logs, which end up in committed + # notebook outputs. See #7. + from morpc_census.geos import geoinfo_from_params + secret = "s3cr3t-api-key" + caplog.set_level("DEBUG") + with patch("morpc_census.geos._get_api_key", return_value=secret), \ + patch("morpc.req.get_json_safely", return_value=[["GEO_ID", "NAME"], ["1600000US3918000", "Columbus"]]) as mock: + geoinfo_from_params({"for": "place:18000", "in": "state:39"}) + assert mock.call_args.kwargs["params"]["key"] == secret + assert secret not in caplog.text