diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb32c6..9f44036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The in-memory profile cache is guarded by a lock (thread-safe under a threaded server), and the captive portal disables the activate button while a request is in flight to prevent double provisioning. +- Request bodies are capped (`MAX_CONTENT_LENGTH` 16 KiB); oversized POSTs get a + 413 instead of being buffered. - `setup_wifi_ap.sh`: the deployed `config.yaml` (holds the OP key) is now `chmod 600` and owned by the service user instead of world-readable, and `data/` is chowned *after* `init_db` so the root-created SQLite file is diff --git a/server/app.py b/server/app.py index 2591a95..595f998 100644 --- a/server/app.py +++ b/server/app.py @@ -69,6 +69,9 @@ def create_app(config_path: str = CONFIG_PATH) -> Flask: portal_dir = os.path.abspath(portal_dir) app = Flask(__name__, static_folder=portal_dir, static_url_path="") + # Provisioning bodies are tiny (a MAC address); cap request size so oversized + # POSTs are rejected with 413 instead of being buffered. + app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 db_path = cfg["database"]["path"] ensure_parent_dir(db_path) diff --git a/tests/test_app.py b/tests/test_app.py index ee511f9..9ccde25 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -86,6 +86,12 @@ class TestProvision: r = client.post("/api/provision", json={"mac_address": "not-a-mac"}) assert r.status_code == 400 + def test_oversized_body_rejected(self, client): + big = '{"mac_address":"' + "a" * (17 * 1024) + '"}' + r = client.post("/api/provision", data=big, + content_type="application/json") + assert r.status_code == 413 + def test_rate_limited_second_request(self, client): client.post("/api/provision", json={"mac_address": "de:ad:be:ef:00:01"}) r = client.post("/api/provision", json={"mac_address": "de:ad:be:ef:00:01"})