Harden: cap request body size (MAX_CONTENT_LENGTH)

Provisioning bodies are tiny; set MAX_CONTENT_LENGTH=16 KiB so oversized POSTs
are rejected with 413 rather than buffered. Tests: +1; 99 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
A ashkan@beigi.net 2026-07-02 02:51:29 +00:00
parent 7c71ab1def
commit c33843d1ec
3 changed files with 11 additions and 0 deletions

View File

@ -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 - 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 threaded server), and the captive portal disables the activate button while
a request is in flight to prevent double provisioning. 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 - `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 `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 `data/` is chowned *after* `init_db` so the root-created SQLite file is

View File

@ -69,6 +69,9 @@ def create_app(config_path: str = CONFIG_PATH) -> Flask:
portal_dir = os.path.abspath(portal_dir) portal_dir = os.path.abspath(portal_dir)
app = Flask(__name__, static_folder=portal_dir, static_url_path="") 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"] db_path = cfg["database"]["path"]
ensure_parent_dir(db_path) ensure_parent_dir(db_path)

View File

@ -86,6 +86,12 @@ class TestProvision:
r = client.post("/api/provision", json={"mac_address": "not-a-mac"}) r = client.post("/api/provision", json={"mac_address": "not-a-mac"})
assert r.status_code == 400 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): def test_rate_limited_second_request(self, client):
client.post("/api/provision", json={"mac_address": "de:ad:be:ef:00:01"}) 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"}) r = client.post("/api/provision", json={"mac_address": "de:ad:be:ef:00:01"})