Enable bounded browser account administration for Phase 8D.7

Allow other-account add/password and forced delete/role commands through
shared dispatcher and handler policy. Keep self-target,
generated-secret,
key, bootstrap, and recovery workflows blocked.

Revalidate currentness after password prompts and before database API
admission. Document that admitted mutations may finish after disconnect,
while subsequent stale operations must reject.

Add policy, transaction-failure, cleanup, and targeted-revocation
regressions. Record completed review, passing host tests and firmware
build, with target validation and M2 acceptance still pending.
This commit is contained in:
2026-09-07 10:03:45 +02:00
parent 326119812f
commit fe1e2d98b4
11 changed files with 533 additions and 13 deletions
+36 -4
View File
@@ -183,6 +183,12 @@ static bool session_is_current(const admin_ssh_console_token_t *token,
return matched && current;
}
bool admin_ssh_console_dispatch_is_current(void)
{
return xTaskGetCurrentTaskHandle() == s_task &&
(!s_dispatch_remote || session_is_current(&s_dispatch_token, &s_dispatch_principal));
}
static bool append_output_locked(admin_session_t *session,
const uint8_t *data, size_t length)
{
@@ -488,6 +494,34 @@ static int ssh_output_write(void *cookie, const char *buffer, int length)
return length;
}
bool admin_ssh_console_web_user_command_allowed(
size_t argc, char **argv, const user_principal_t *principal)
{
if (argc == 0U || strcmp(argv[0], "user") != 0) return false;
if (argc == 1U ||
(argc == 2U && (strcmp(argv[1], "status") == 0 ||
strcmp(argv[1], "list") == 0)) ||
(argc == 3U && strcmp(argv[1], "show") == 0)) return true;
bool role_valid = argc >= 4U &&
(strcmp(argv[3], "user") == 0 || strcmp(argv[3], "admin") == 0);
bool mutation =
(argc == 4U && strcmp(argv[1], "add") == 0 && role_valid) ||
(argc == 3U && strcmp(argv[1], "password") == 0) ||
(argc == 4U && strcmp(argv[1], "delete") == 0 &&
strcmp(argv[3], "--force") == 0) ||
(argc == 5U && strcmp(argv[1], "role") == 0 && role_valid &&
strcmp(argv[4], "--force") == 0);
/* Parsed names use the database's exact, case-sensitive identity. Reject
* self even for no-op role changes; their handler still requests revocation.
* Generation/output and key workflows remain outside this bounded slice. */
return mutation && principal != NULL && principal->role == USER_ROLE_ADMIN &&
principal->username_length > 0U &&
principal->username_length <= USER_DATABASE_USERNAME_CAPACITY &&
!(strlen(argv[2]) == principal->username_length &&
memcmp(argv[2], principal->username, principal->username_length) == 0);
}
static bool remote_command_allowed(const admin_request_t *request)
{
char copy[ADMIN_SSH_CONSOLE_COMMAND_LINE_CAPACITY + 1U];
@@ -514,10 +548,8 @@ static bool remote_command_allowed(const admin_request_t *request)
} else if (strcmp(argv[0], "wifi") == 0 || strcmp(argv[0], "mdns") == 0) {
allowed = argc == 2U && strcmp(argv[1], "status") == 0;
} else if (strcmp(argv[0], "user") == 0) {
allowed = argc == 1U ||
(argc == 2U && (strcmp(argv[1], "status") == 0 ||
strcmp(argv[1], "list") == 0)) ||
(argc == 3U && strcmp(argv[1], "show") == 0);
allowed = admin_ssh_console_web_user_command_allowed(
argc, argv, &request->principal);
} else if (strcmp(argv[0], "reboot") == 0) {
allowed = argc == 1U;
} else if (strcmp(argv[0], "ssh") == 0 && argc >= 2U) {
+6
View File
@@ -121,6 +121,12 @@ esp_err_t admin_ssh_console_start_uart_frontend(void);
bool admin_ssh_console_dispatch_is_remote(void);
bool admin_ssh_console_dispatch_is_web(void);
const user_principal_t *admin_ssh_console_dispatch_principal(void);
/* Revalidate account, originating owner/session and token before side effects.
* False outside the dispatcher; UART0 dispatch remains physically trusted. */
bool admin_ssh_console_dispatch_is_current(void);
/* Shared parsed browser account policy: dispatcher admission + handler defense. */
bool admin_ssh_console_web_user_command_allowed(
size_t argc, char **argv, const user_principal_t *principal);
esp_err_t admin_ssh_console_dispatch_read_input(
const char *prompt, uint8_t *output, size_t capacity,
bool hidden, size_t *output_length);
+27 -3
View File
@@ -219,6 +219,15 @@ static int bootstrap(bool generated)
return 0;
}
static esp_err_t mutation_currentness(void)
{
if (admin_ssh_console_dispatch_is_remote() &&
!admin_ssh_console_dispatch_is_current()) {
return ESP_ERR_NOT_ALLOWED;
}
return ESP_OK;
}
static int add_user(const char *username, const char *role_text, bool generated)
{
user_role_t role;
@@ -239,6 +248,7 @@ static int add_user(const char *username, const char *role_text, bool generated)
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) error = mutation_currentness();
if (error == ESP_OK) {
error = user_database_create((const uint8_t *)username, strlen(username),
role, password, password_length);
@@ -268,6 +278,7 @@ static int change_password(const char *username, bool generated)
uint8_t password[USER_DATABASE_PASSWORD_CAPACITY + 1U] = {0};
size_t password_length = 0U;
error = read_password(password, &password_length);
if (error == ESP_OK) error = mutation_currentness();
if (error == ESP_OK) {
error = user_database_set_password((const uint8_t *)username,
strlen(username),
@@ -388,6 +399,14 @@ static int command_user_inner(int argc, char **argv)
{
bool remote = admin_ssh_console_dispatch_is_remote();
const user_principal_t *principal = admin_ssh_console_dispatch_principal();
/* Repeat admission on canonical parsed arguments: direct handler calls must
* not bypass self-target, generated-secret or UART0-only restrictions. */
if (admin_ssh_console_dispatch_is_web() &&
(!admin_ssh_console_web_user_command_allowed((size_t)argc, argv, principal) ||
!admin_ssh_console_dispatch_is_current())) {
printf("Browser account command restricted or session no longer current.\n");
return 1;
}
if (argc == 1 || (argc == 2 && strcmp(argv[1], "status") == 0) ||
(argc == 2 && strcmp(argv[1], "list") == 0)) {
return show_users(NULL);
@@ -425,7 +444,10 @@ static int command_user_inner(int argc, char **argv)
}
if (argc == 4 && strcmp(argv[1], "delete") == 0 &&
strcmp(argv[3], "--force") == 0) {
esp_err_t error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
esp_err_t error = mutation_currentness();
if (error == ESP_OK) {
error = user_database_delete((const uint8_t *)argv[2], strlen(argv[2]));
}
if (error != ESP_OK) {
printf("Could not delete user (the migrated or final admin is protected): %s\n",
esp_err_to_name(error));
@@ -443,8 +465,10 @@ static int command_user_inner(int argc, char **argv)
printf("Role must be user or admin.\n");
return 1;
}
esp_err_t error = user_database_set_role((const uint8_t *)argv[2],
strlen(argv[2]), role);
esp_err_t error = mutation_currentness();
if (error == ESP_OK) {
error = user_database_set_role((const uint8_t *)argv[2], strlen(argv[2]), role);
}
if (error != ESP_OK) {
printf("Could not change role (the final admin is protected): %s\n",
esp_err_to_name(error));