Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Scope): sign-in pages are now an allowed destination, only while the user runs `

| Change | Security review |
|---|---|
| New outbound traffic: a plain-HTTP request to `connectivitycheck.gstatic.com/generate_204` (if that name doesn't resolve: the network's own gateway, `http://<gateway>/`), then the pages, links and form targets the user picks | Only on the user's request, never automatically (CI guard). Plain HTTP is used for the probe alone, because a portal can only intercept unencrypted traffic; a 204 answer means the internet works. Every HTTPS page is verified with the ESP-IDF CA bundle: a portal with a self-signed certificate fails to load rather than being trusted. Redirects are followed by hand, at most 8, only to http(s) URLs without `user@` parts (IPv4 / DNS names only) |
| New outbound traffic: a plain-HTTP request to `connectivitycheck.gstatic.com/generate_204` (if that name doesn't resolve: the network's own gateway, `http://<gateway>/`), then the pages, links and form targets the user picks | Only on the user's request, never automatically (CI guard). Plain HTTP is used for the probe alone, because a portal can only intercept unencrypted traffic; a 204 answer means the internet works. Every HTTPS page is verified with the ESP-IDF CA bundle: a portal with a self-signed certificate fails to load rather than being trusted. Redirects are followed by hand, at most 8, only to http(s) URLs (a 307/308 redirect never carries a submitted form from HTTPS to plain HTTP) without `user@` parts (IPv4 / DNS names only) |
| HTML parser for untrusted pages (`main/portal.cpp`, host-tested with hostile and truncated input under ASan/UBSan) | No scripts run; only a literal `location = "..."` or meta refresh is followed, and only on a page with nothing to fill in. Body capped at 192 KB in PSRAM (zeroed before it is freed); at most 8 forms, 60 fields, 40 options per list, 12 links, 1500 characters of text, 512-byte values, 1 KB URLs. Text reaches the screen as printable ASCII (no control or escape characters). `javascript:`, `data:` and other schemes are dropped |
| Cookies | Kept in RAM for the sign-in only, at most 30 of 1 KB; a cookie is only accepted for the host that set it or its parent domain (not a bare top-level domain), `Secure` cookies only go over HTTPS, and the jar is wiped when the sign-in ends |
| A worker task (10 KB stack) per page load | Created per request and deleted when done, like the ChatGPT worker; it touches the menu only under the display lock and drops its result if the user left the sign-in (generation counter) |
Expand Down
6 changes: 6 additions & 0 deletions main/portal_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,12 @@ esp_err_t fetch(const Request& request, Cookies& cookies, Result& out)
wipe(req.body); // 301/302/303: the next request is a plain GET
req.post = false;
}
Url target;
if (req.post && url.https && parse_url(next, target) && !target.https) {
out.error = "The page tried to send what you typed over an unencrypted connection."; // Never https -> http with a body
wipe(req);
return ESP_FAIL;
}
req.referer = url.str();
req.url = next;
continue;
Expand Down
Loading