Replace Web Basic Auth With Cookie Sessions

Add bounded login challenges, CSRF/origin enforcement, logout, and
session-bound WebSocket admission. Isolate private HTTPD access behind a
version-guarded adapter and add focused host coverage. Also let empty
admin
SSH input reach the normal console handler.
This commit is contained in:
2026-09-05 23:55:05 +02:00
parent 4435a7fddd
commit 5a609fa40b
36 changed files with 1940 additions and 360 deletions
+38 -16
View File
@@ -14,6 +14,8 @@
#include "sdkconfig.h"
#include "secure_random.h"
#include "serial_service.h"
#include "web_auth_parse.h"
#include "web_httpd_adapter.h"
#if !defined(CONFIG_HTTPD_WS_SUPPORT) || !CONFIG_HTTPD_WS_SUPPORT
#error "web_serial_transport requires CONFIG_HTTPD_WS_SUPPORT"
@@ -104,8 +106,7 @@ static uint64_t s_ticket_epoch;
static esp_err_t identity_is_current(const user_principal_t *principal,
web_session_id_t id, bool *current)
{
return id == 0U ? user_database_principal_is_current(principal, current)
: web_session_store_check_principal(id, principal, current);
return web_session_store_check_principal(id, principal, current);
}
static TickType_t milliseconds_to_ticks(uint32_t milliseconds)
@@ -270,9 +271,6 @@ static esp_err_t validate_origin(httpd_req_t *request)
esp_err_t result = httpd_req_get_hdr_value_str(
request, "Origin", origin, sizeof(origin));
if (result == ESP_ERR_NOT_FOUND) {
return ESP_OK;
}
if (result != ESP_OK) {
return ESP_ERR_INVALID_ARG;
}
@@ -284,9 +282,7 @@ static esp_err_t validate_origin(httpd_req_t *request)
return ESP_ERR_INVALID_ARG;
}
int written = snprintf(expected, sizeof(expected), "https://%s", host);
bool matches = written > 0 && (size_t)written < sizeof(expected) &&
strcmp(origin, expected) == 0;
bool matches = web_auth_parse_origin(host, strlen(host), origin, strlen(origin), expected);
secure_wipe(origin, sizeof(origin));
secure_wipe(host, sizeof(host));
secure_wipe(expected, sizeof(expected));
@@ -652,6 +648,12 @@ static esp_err_t connect_websocket(httpd_req_t *request, int socket_fd,
}
bool activated = false;
/* Ticket and principal admission must succeed before HTTP 101. */
result = web_httpd_upgrade(request, web_serial_transport_ws_handler);
if (result != ESP_OK) {
close_unpublished_broker_session(slot, slot_generation, client_id);
goto cleanup;
}
int64_t next_currentness_check_us =
monotonic_time_us() + WEB_SERIAL_CURRENTNESS_INTERVAL_US;
taskENTER_CRITICAL(&s_lock);
@@ -1576,6 +1578,26 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
return ESP_ERR_INVALID_STATE;
}
/* Reclaim stale identities without database calls under the transport lock.
* A late result must not clear a ticket published into the same array slot. */
for (size_t i = 0; i < WEB_SERIAL_TRANSPORT_MAX_TICKETS; ++i) {
taskENTER_CRITICAL(&s_lock);
web_serial_ticket_t candidate = s_tickets[i];
taskEXIT_CRITICAL(&s_lock);
bool live = false;
if (candidate.active &&
(identity_is_current(&candidate.principal, candidate.web_session_id, &live) != ESP_OK || !live)) {
taskENTER_CRITICAL(&s_lock);
web_serial_ticket_t *entry = &s_tickets[i];
if (entry->active && entry->web_session_id == candidate.web_session_id &&
entry->expires_at_us == candidate.expires_at_us &&
constant_time_equal(entry->digest, candidate.digest, sizeof(entry->digest)))
clear_ticket_locked(entry);
taskEXIT_CRITICAL(&s_lock);
}
secure_wipe(&candidate, sizeof(candidate));
}
uint8_t random_bytes[WEB_SERIAL_RANDOM_BYTES] = {0};
uint8_t digest[WEB_SERIAL_SHA256_BYTES] = {0};
result = secure_random_fill(random_bytes, sizeof(random_bytes));
@@ -1602,7 +1624,6 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
epoch != UINT64_MAX) {
purge_tickets_locked(now_us);
size_t selected = WEB_SERIAL_TRANSPORT_MAX_TICKETS;
int64_t oldest_expiry = INT64_MAX;
for (size_t index = 0U; index < WEB_SERIAL_TRANSPORT_MAX_TICKETS;
++index) {
web_serial_ticket_t *entry = &s_tickets[index];
@@ -1610,10 +1631,6 @@ esp_err_t web_serial_transport_mint_ticket(const user_principal_t *principal,
selected = index;
break;
}
if (entry->expires_at_us < oldest_expiry) {
oldest_expiry = entry->expires_at_us;
selected = index;
}
}
if (selected < WEB_SERIAL_TRANSPORT_MAX_TICKETS) {
web_serial_ticket_t *entry = &s_tickets[selected];
@@ -1688,6 +1705,9 @@ esp_err_t web_serial_transport_handle_authenticated_ticket_request(
if (result == ESP_OK) {
result = httpd_resp_set_hdr(request, "X-Content-Type-Options", "nosniff");
}
if (result == ESP_OK) {
result = httpd_resp_set_hdr(request, "Referrer-Policy", "no-referrer");
}
if (result == ESP_OK) {
result = httpd_resp_send(request, response, written);
}
@@ -1715,12 +1735,14 @@ esp_err_t web_serial_transport_session_ws_handler(httpd_req_t *request,
httpd_ws_client_info_t info =
httpd_ws_get_fd_info(request->handle, socket_fd);
if (info == HTTPD_WS_CLIENT_HTTP) {
bool opening = request->sess_ctx == NULL && web_session_id != 0U &&
request->method == HTTP_GET && web_httpd_upgrade_requested(request);
if (info == HTTPD_WS_CLIENT_HTTP && !opening) {
(void)send_plain_bad_request(request);
add_counter(&s_counters.protocol_errors, 1U);
return ESP_FAIL;
}
if (info != HTTPD_WS_CLIENT_WEBSOCKET) {
if (info != HTTPD_WS_CLIENT_WEBSOCKET && !opening) {
return ESP_FAIL;
}
@@ -1736,7 +1758,7 @@ esp_err_t web_serial_transport_session_ws_handler(httpd_req_t *request,
esp_err_t result;
if (request->sess_ctx == NULL) {
/* IDF has already sent 101; authentication failures must only close. */
/* The registered HTTP route defers 101 until admission. */
result = connect_websocket(request, socket_fd, web_session_id);
} else {
result = process_websocket_frame(request);