From 473690176ac942d6338f7aa49634564ae67f3510 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 18 Sep 2026 11:25:44 +0000 Subject: [PATCH] fix(gateway): fail closed when auth_pair bearer RNG fails auth_pair ignored generate_random_hex failures, so urandom/OOM left an uninitialized stack buffer as the bearer, wrote it to auth_tokens.json, and burned the pairing code. Check the RNG result before mutating store state and keep the pending code usable for retry. Co-authored-by: esadrianno --- CHANGELOG.md | 1 + src/gateway/auth.c | 4 +++- tests/test_auth.c | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 901fb38..63300f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ All notable changes to ShellClaw are documented here. Format follows [Keep a Cha - Gateway `/health` `version` matches `SHELLCLAW_RELEASE_VERSION`. ### Security +- `auth_pair` fails closed when bearer RNG fails (no uninitialized token, no tokens-file write, pairing code kept). - Camera auto-output keeps the exclusive `mkstemp` inode (no unlink + `${tmpl}.jpg` sibling). - Reject I2C `bus` outside 0–255 at the tool JSON boundary. diff --git a/src/gateway/auth.c b/src/gateway/auth.c index 578066e..1c9c495 100644 --- a/src/gateway/auth.c +++ b/src/gateway/auth.c @@ -174,7 +174,9 @@ int auth_pair(auth_ctx_t *ctx, const char *code, char *token_out, size_t token_s !constant_time_cmp(code, ctx->pending_pairing_code, PAIRING_CODE_LEN)) return -1; char new_token[TOKEN_LEN + 1]; - generate_random_hex(new_token, TOKEN_LEN); + /* Fail closed: never persist or return an uninitialized bearer on RNG/OOM. */ + if (generate_random_hex(new_token, TOKEN_LEN) != 0) + return -1; /* Read existing tokens and append (multi-device support). */ cJSON *arr = NULL; { diff --git a/tests/test_auth.c b/tests/test_auth.c index aa7f48d..fee8c56 100644 --- a/tests/test_auth.c +++ b/tests/test_auth.c @@ -5,10 +5,12 @@ #define _POSIX_C_SOURCE 200809L #include "gateway/auth.h" +#include "crypto/crypto.h" #include "cJSON.h" #include #include #include +#include #include #define ASSERT(c) do { if (!(c)) { fprintf(stderr, "FAIL: %s:%d %s\n", __FILE__, __LINE__, #c); return 1; } } while (0) @@ -302,6 +304,47 @@ static int test_pair_lockout_null_ip(void) return 0; } +/** + * auth_pair must fail closed when bearer RNG fails: do not return success, + * do not write auth_tokens.json, and keep the pending pairing code usable. + */ +static int test_auth_pair_fails_closed_on_urandom_failure(void) +{ + const char *path = "/tmp/shellclaw_test_tokens_urandom_fail.json"; + auth_ctx_t *ctx; + char *code; + char token[64]; + struct stat st; + int paired_after; + + unlink(path); + ctx = auth_init(path); + ASSERT(ctx != NULL); + code = auth_get_or_create_pairing_code(ctx); + ASSERT(code != NULL); + + memset(token, 0x41, sizeof(token)); + token[sizeof(token) - 1] = '\0'; + crypto_test_force_urandom_fail(1); + ASSERT(auth_pair(ctx, code, token, sizeof(token)) != 0); + crypto_test_clear_force_urandom_fail(); + + /* Must not leave a success-looking empty/garbage bearer or tokens file. */ + ASSERT(token[0] == 'A'); + ASSERT(stat(path, &st) != 0); + + memset(token, 0, sizeof(token)); + paired_after = auth_pair(ctx, code, token, sizeof(token)); + ASSERT(paired_after == 0); + ASSERT(strlen(token) == TEST_TOKEN_HEX_LEN); + ASSERT(auth_validate_token(ctx, token) == 1); + + free(code); + auth_cleanup(ctx); + unlink(path); + return 0; +} + static int test_auth_pair_rejects_malformed_code(void) { const char *path = "/tmp/shellclaw_test_tokens_malformed.json"; @@ -469,6 +512,7 @@ int main(void) if (test_pair_lockout_clear_on_success() != 0) { fprintf(stderr, "test_pair_lockout_clear_on_success failed\n"); failed++; } if (test_pair_lockout_independent_ips() != 0) { fprintf(stderr, "test_pair_lockout_independent_ips failed\n"); failed++; } if (test_pair_lockout_null_ip() != 0) { fprintf(stderr, "test_pair_lockout_null_ip failed\n"); failed++; } + if (test_auth_pair_fails_closed_on_urandom_failure() != 0) { fprintf(stderr, "test_auth_pair_fails_closed_on_urandom_failure failed\n"); failed++; } if (test_auth_pair_rejects_malformed_code() != 0) { fprintf(stderr, "test_auth_pair_rejects_malformed_code failed\n"); failed++; } if (test_auth_pair_evicts_oldest_at_cap() != 0) { fprintf(stderr, "test_auth_pair_evicts_oldest_at_cap failed\n"); failed++; } if (test_auth_validate_token_rejects_length_mismatch() != 0) { fprintf(stderr, "test_auth_validate_token_rejects_length_mismatch failed\n"); failed++; }