Add allocation-free web auth parsers
Implement strict origin, cookie, and login JSON parsing with fail-closed validation and output wiping. Add focused host contract tests and document the preparatory 8D.3 parser split.
This commit is contained in:
@@ -35,6 +35,7 @@ idf_component_register(
|
||||
"web_ui.c"
|
||||
"web_server.c"
|
||||
"web_session_store.c"
|
||||
"web_auth_parse.c"
|
||||
"web_console.c"
|
||||
"wifi_config.c"
|
||||
"wifi_manager.c"
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
#include "web_auth_parse.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static void wipe(void *buffer, size_t length)
|
||||
{
|
||||
volatile uint8_t *p = buffer;
|
||||
while (length--) *p++ = 0;
|
||||
}
|
||||
|
||||
static bool alnum_ascii(unsigned char c)
|
||||
{
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
static bool authority(const char *text, size_t length, char *out)
|
||||
{
|
||||
if (!text || !length || length > WEB_AUTH_ORIGIN_CAPACITY - 5U) return false;
|
||||
if (length >= 4U && memcmp(text + length - 4U, ":443", 4U) == 0) length -= 4U;
|
||||
if (!length || length > WEB_AUTH_ORIGIN_CAPACITY - 9U) return false;
|
||||
size_t label = 0;
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
unsigned char c = (unsigned char)text[i];
|
||||
if (c == '.') {
|
||||
if (!label || text[i - 1U] == '-') return false;
|
||||
label = 0;
|
||||
} else {
|
||||
if (!alnum_ascii(c) && c != '-') return false;
|
||||
if ((!label && c == '-') || ++label > 63U) return false;
|
||||
}
|
||||
out[i] = c >= 'A' && c <= 'Z' ? (char)(c + ('a' - 'A')) : (char)c;
|
||||
}
|
||||
if (!label || text[length - 1U] == '-') return false;
|
||||
out[length] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool web_auth_parse_origin(const char *host, size_t host_length,
|
||||
const char *origin, size_t origin_length,
|
||||
char canonical[WEB_AUTH_ORIGIN_CAPACITY])
|
||||
{
|
||||
if (!canonical) return false;
|
||||
memset(canonical, 0, WEB_AUTH_ORIGIN_CAPACITY);
|
||||
char other[WEB_AUTH_ORIGIN_CAPACITY] = {0};
|
||||
if (!origin || origin_length < 9U || origin_length > WEB_AUTH_ORIGIN_CAPACITY + 3U ||
|
||||
memcmp(origin, "https://", 8U) != 0 ||
|
||||
!authority(host, host_length, canonical + 8U) ||
|
||||
!authority(origin + 8U, origin_length - 8U, other) ||
|
||||
strcmp(canonical + 8U, other) != 0) {
|
||||
memset(canonical, 0, WEB_AUTH_ORIGIN_CAPACITY);
|
||||
return false;
|
||||
}
|
||||
memcpy(canonical, "https://", 8U);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool cookie_name_char(unsigned char c)
|
||||
{
|
||||
return alnum_ascii(c) || (c && strchr("!#$%&'*+-.^_`|~", c));
|
||||
}
|
||||
|
||||
bool web_auth_parse_cookie(const char *header, size_t length, const char *name,
|
||||
char token[WEB_AUTH_TOKEN_LENGTH + 1U])
|
||||
{
|
||||
if (!token) return false;
|
||||
memset(token, 0, WEB_AUTH_TOKEN_LENGTH + 1U);
|
||||
if (!header || !name || !*name || !length || length > WEB_AUTH_COOKIE_HEADER_MAX)
|
||||
return false;
|
||||
size_t pos = 0, selected = 0, name_length = strlen(name);
|
||||
bool found = false;
|
||||
while (pos < length) {
|
||||
while (pos < length && header[pos] == ' ') ++pos;
|
||||
size_t start = pos;
|
||||
while (pos < length && cookie_name_char((unsigned char)header[pos])) ++pos;
|
||||
size_t key_length = pos - start;
|
||||
if (!key_length || pos == length || header[pos++] != '=') return false;
|
||||
size_t value = pos;
|
||||
while (pos < length && header[pos] != ';') {
|
||||
unsigned char c = (unsigned char)header[pos++];
|
||||
if (c < 0x21 || c > 0x7e || c == '"' || c == ',' || c == '\\') return false;
|
||||
}
|
||||
if (key_length == name_length && memcmp(header + start, name, key_length) == 0) {
|
||||
if (found || pos - value != WEB_AUTH_TOKEN_LENGTH) return false;
|
||||
for (size_t i = value; i < pos; ++i)
|
||||
if (!((header[i] >= '0' && header[i] <= '9') ||
|
||||
(header[i] >= 'a' && header[i] <= 'f'))) return false;
|
||||
found = true;
|
||||
selected = value;
|
||||
}
|
||||
if (pos < length && ++pos == length) return false;
|
||||
}
|
||||
if (found) memcpy(token, header + selected, WEB_AUTH_TOKEN_LENGTH);
|
||||
return found;
|
||||
}
|
||||
|
||||
typedef struct { const uint8_t *data; size_t length; size_t pos; } json_cursor_t;
|
||||
|
||||
static void whitespace(json_cursor_t *c)
|
||||
{
|
||||
while (c->pos < c->length) {
|
||||
uint8_t b = c->data[c->pos];
|
||||
if (b != ' ' && b != '\t' && b != '\r' && b != '\n') break;
|
||||
++c->pos;
|
||||
}
|
||||
}
|
||||
|
||||
static bool take(json_cursor_t *c, uint8_t byte)
|
||||
{
|
||||
whitespace(c);
|
||||
if (c->pos == c->length || c->data[c->pos] != byte) return false;
|
||||
++c->pos;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool hex4(json_cursor_t *c, uint32_t *value)
|
||||
{
|
||||
*value = 0;
|
||||
for (unsigned i = 0; i < 4; ++i) {
|
||||
if (c->pos == c->length) return false;
|
||||
uint8_t b = c->data[c->pos++];
|
||||
unsigned digit;
|
||||
if (b >= '0' && b <= '9') digit = b - '0';
|
||||
else if (b >= 'a' && b <= 'f') digit = b - 'a' + 10U;
|
||||
else if (b >= 'A' && b <= 'F') digit = b - 'A' + 10U;
|
||||
else return false;
|
||||
*value = (*value << 4) | digit;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool codepoint(json_cursor_t *c, uint32_t *value)
|
||||
{
|
||||
if (c->pos == c->length) return false;
|
||||
uint8_t b = c->data[c->pos++];
|
||||
if (b == '\\') {
|
||||
if (c->pos == c->length) return false;
|
||||
b = c->data[c->pos++];
|
||||
switch (b) {
|
||||
case '"': case '\\': case '/': *value = b; return true;
|
||||
case 'b': *value = 8; return true;
|
||||
case 'f': *value = 12; return true;
|
||||
case 'n': *value = 10; return true;
|
||||
case 'r': *value = 13; return true;
|
||||
case 't': *value = 9; return true;
|
||||
case 'u': break;
|
||||
default: return false;
|
||||
}
|
||||
if (!hex4(c, value)) return false;
|
||||
if (*value >= 0xd800 && *value <= 0xdbff) {
|
||||
uint32_t low;
|
||||
if (c->length - c->pos < 2U || c->data[c->pos++] != '\\' ||
|
||||
c->data[c->pos++] != 'u' || !hex4(c, &low) ||
|
||||
low < 0xdc00 || low > 0xdfff) return false;
|
||||
*value = 0x10000 + ((*value - 0xd800) << 10) + low - 0xdc00;
|
||||
}
|
||||
return *value && !(*value >= 0xd800 && *value <= 0xdfff);
|
||||
}
|
||||
if (b < 0x20) return false;
|
||||
if (b < 0x80) { *value = b; return true; }
|
||||
unsigned extra;
|
||||
uint32_t minimum;
|
||||
if (b >= 0xc2 && b <= 0xdf) { extra = 1; minimum = 0x80; *value = b & 0x1f; }
|
||||
else if (b >= 0xe0 && b <= 0xef) { extra = 2; minimum = 0x800; *value = b & 0x0f; }
|
||||
else if (b >= 0xf0 && b <= 0xf4) { extra = 3; minimum = 0x10000; *value = b & 7; }
|
||||
else return false;
|
||||
while (extra--) {
|
||||
if (c->pos == c->length) return false;
|
||||
b = c->data[c->pos++];
|
||||
if ((b & 0xc0) != 0x80) return false;
|
||||
*value = (*value << 6) | (b & 0x3f);
|
||||
}
|
||||
return *value >= minimum && *value <= 0x10ffff &&
|
||||
!(*value >= 0xd800 && *value <= 0xdfff);
|
||||
}
|
||||
|
||||
static bool string(json_cursor_t *c, uint8_t *out, size_t capacity, size_t *length)
|
||||
{
|
||||
*length = 0;
|
||||
if (!take(c, '"')) return false;
|
||||
while (c->pos < c->length && c->data[c->pos] != '"') {
|
||||
uint32_t cp;
|
||||
if (!codepoint(c, &cp)) return false;
|
||||
size_t bytes = cp < 0x80 ? 1U : cp < 0x800 ? 2U : cp < 0x10000 ? 3U : 4U;
|
||||
if (bytes > capacity - *length) return false;
|
||||
if (bytes == 1U) out[(*length)++] = (uint8_t)cp;
|
||||
else {
|
||||
out[(*length)++] = (uint8_t)((bytes == 2U ? 0xc0 : bytes == 3U ? 0xe0 : 0xf0) |
|
||||
(cp >> (6U * (bytes - 1U))));
|
||||
for (size_t i = bytes - 1U; i > 0; --i)
|
||||
out[(*length)++] = (uint8_t)(0x80 | ((cp >> (6U * (i - 1U))) & 0x3f));
|
||||
}
|
||||
}
|
||||
if (c->pos == c->length) return false;
|
||||
++c->pos;
|
||||
out[*length] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool web_auth_parse_login(const char *body, size_t length,
|
||||
web_auth_credentials_t *credentials)
|
||||
{
|
||||
if (!credentials) return false;
|
||||
wipe(credentials, sizeof(*credentials));
|
||||
if (!body || !length || length > WEB_AUTH_LOGIN_BODY_MAX) return false;
|
||||
json_cursor_t c = { (const uint8_t *)body, length, 0 };
|
||||
unsigned seen = 0;
|
||||
if (!take(&c, '{')) return false;
|
||||
for (unsigned field = 0; field < 2; ++field) {
|
||||
uint8_t key[9] = {0};
|
||||
size_t key_length;
|
||||
if ((field && !take(&c, ',')) || !string(&c, key, 8U, &key_length) ||
|
||||
!take(&c, ':')) goto invalid;
|
||||
unsigned bit;
|
||||
uint8_t *output;
|
||||
size_t *output_length, capacity;
|
||||
if (key_length == 8U && memcmp(key, "username", 8U) == 0) {
|
||||
bit = 1; output = credentials->username;
|
||||
output_length = &credentials->username_length; capacity = WEB_AUTH_USERNAME_MAX;
|
||||
} else if (key_length == 8U && memcmp(key, "password", 8U) == 0) {
|
||||
bit = 2; output = credentials->password;
|
||||
output_length = &credentials->password_length; capacity = WEB_AUTH_PASSWORD_MAX;
|
||||
} else goto invalid;
|
||||
if ((seen & bit) || !string(&c, output, capacity, output_length)) goto invalid;
|
||||
seen |= bit;
|
||||
}
|
||||
if (!take(&c, '}')) goto invalid;
|
||||
whitespace(&c);
|
||||
if (c.pos == c.length && seen == 3U) return true;
|
||||
invalid:
|
||||
wipe(credentials, sizeof(*credentials));
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
/* Private, allocation-free parsing only: these helpers do not authorize requests. */
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define WEB_AUTH_ORIGIN_CAPACITY 129U
|
||||
#define WEB_AUTH_COOKIE_HEADER_MAX 1024U
|
||||
#define WEB_AUTH_TOKEN_LENGTH 64U
|
||||
#define WEB_AUTH_LOGIN_BODY_MAX 512U
|
||||
#define WEB_AUTH_USERNAME_MAX 16U
|
||||
#define WEB_AUTH_PASSWORD_MAX 64U
|
||||
|
||||
typedef struct {
|
||||
size_t username_length;
|
||||
size_t password_length;
|
||||
uint8_t username[WEB_AUTH_USERNAME_MAX + 1U];
|
||||
uint8_t password[WEB_AUTH_PASSWORD_MAX + 1U];
|
||||
} web_auth_credentials_t;
|
||||
|
||||
/* Exact byte spans, not necessarily NUL-terminated. Inputs and output must not
|
||||
* alias. Failures clear output. Host supports ASCII DNS/IPv4 authorities only;
|
||||
* IPv6 literals are deliberately rejected until the device supports that route.
|
||||
* Only optional :443 is accepted. Origin is mandatory and must match Host.
|
||||
* HTTP callers must separately reject duplicate header lines, enforce methods,
|
||||
* body/content-type limits, Fetch Metadata and CSRF/session policy. */
|
||||
bool web_auth_parse_origin(const char *host, size_t host_length,
|
||||
const char *origin, size_t origin_length,
|
||||
char canonical[WEB_AUTH_ORIGIN_CAPACITY]);
|
||||
/* Extract exactly one named lowercase-hex token; malformed/duplicate or missing
|
||||
* selected cookie fails. Other cookies are syntax-checked but not retained.
|
||||
* This deliberately accepts only unquoted cookie values, including unrelated
|
||||
* cookies; quoted values fail closed. No whitespace inside a cookie pair.
|
||||
* name is a trusted, nonempty C string. Output is sensitive: wipe after use. */
|
||||
bool web_auth_parse_cookie(const char *header, size_t length, const char *name,
|
||||
char token[WEB_AUTH_TOKEN_LENGTH + 1U]);
|
||||
/* Exactly username/password string fields, either order. JSON escapes and valid
|
||||
* UTF-8 accepted; unknown/duplicate fields, NUL and malformed Unicode rejected.
|
||||
* Database credential policy remains authoritative. Caller must wipe BOTH the
|
||||
* original request body and successful credentials using secure_wipe(). */
|
||||
bool web_auth_parse_login(const char *body, size_t length,
|
||||
web_auth_credentials_t *credentials);
|
||||
Reference in New Issue
Block a user