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