Author: Andy Green Date: Sun Sep 06 08:53:02 2026 +0100 server: validate builder artifact task_uuid, escape taskchange JSON, fixes F-011 sais_taskchange()/sais_eventchange() compose the sai-taskchange / sai-eventchange JSON handed to every sai-web (and from there to every browser) with lws_snprintf and no escaping. Every browser-initiated caller gates ids through sais_validate_id, but the builder link does not: a hostile builder's artifact-upload JSON carries a builder-chosen task_uuid that reaches sais_taskchange() verbatim, allowing chosen JSON members into browser sessions and sai-web db filters if crafted to stay parseable, or repeated web-link teardown (the F-002 failure mode) if not. - validate the artifact-upload task_uuid at intake in s-ws-builder.c with sais_validate_id(.., SAI_TASKID_LEN): it decides which event db is opened and feeds queries and broadcasts, so it must be a real server-minted task id. sais_validate_id moved from s-ws-web.c to s-helpers.c and declared in s-private.h for reuse. - compose taskchange/eventchange with lws_json_purify() so the broadcast helpers cannot be fed injected JSON by any future caller. - the two artifact-path SQL literals using the builder-supplied uuid in double-quote delimiters (which lws_sql_purify does not escape, the F-007 class noted on that finding) are single-quoted to match the purifier. diff --git a/src/server/s-helpers.c b/src/server/s-helpers.c index 0cb85d7..d4907e9 100644 --- a/src/server/s-helpers.c +++ b/src/server/s-helpers.c @@ -37,6 +37,34 @@ #include "s-private.h" +/* + * Ids that came in from outside (browser or builder link) must be exactly + * the length of the server-minted id kind and purely alnum, before they are + * used in queries, db filenames or broadcasts. + */ +int +sais_validate_id(const char *id, int reqlen) +{ + const char *idin = id; + int n = reqlen; + + while (*id && n--) { + if (!((*id >= '0' && *id <= '9') || + (*id >= 'a' && *id <= 'z') || + (*id >= 'A' && *id <= 'Z'))) + goto reject; + id++; + } + + if (!n && !*id) + return 0; +reject: + + lwsl_notice("%s: Invalid ID (%d) '%s'\n", __func__, reqlen, idin); + + return 1; +} + int sql3_get_integer_cb(void *user, int cols, char **values, char **name) { diff --git a/src/server/s-private.h b/src/server/s-private.h index 69885aa..ea0688b 100644 --- a/src/server/s-private.h +++ b/src/server/s-private.h @@ -284,6 +284,9 @@ int sai_sql3_get_uint64_cb(void *user, int cols, char **values, char **name); int +sais_validate_id(const char *id, int reqlen); + +int saiw_ws_json_tx_browser(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t bl); int diff --git a/src/server/s-webops.c b/src/server/s-webops.c index cc03fd3..6bf1943 100644 --- a/src/server/s-webops.c +++ b/src/server/s-webops.c @@ -163,7 +163,7 @@ sais_websrv_broadcast_buflist(struct lws_ss_handle *hsrv, struct lws_buflist **b void sais_taskchange(struct lws_ss_handle *hsrv, const char *task_uuid, int state) { - char tc[LWS_PRE + 256], *start = tc + LWS_PRE; + char tc[LWS_PRE + 256], *start = tc + LWS_PRE, esc[256]; lws_wsmsg_info_t info; int n; @@ -172,7 +172,8 @@ sais_taskchange(struct lws_ss_handle *hsrv, const char *task_uuid, int state) n = lws_snprintf(start, sizeof(tc) - LWS_PRE, "{\"schema\":\"sai-taskchange\", " "\"event_hash\":\"%s\", \"state\":%d}", - task_uuid, state); + lws_json_purify(esc, task_uuid, sizeof(esc) - 1, NULL), + state); memset(&info, 0, sizeof(info)); info.private_source_idx = SAI_WEBSRV_PB__GENERATED; @@ -190,7 +191,7 @@ sais_taskchange(struct lws_ss_handle *hsrv, const char *task_uuid, int state) void sais_eventchange(struct lws_ss_handle *hsrv, const char *event_uuid, int state) { - char tc[LWS_PRE + 256], *start = tc + LWS_PRE; + char tc[LWS_PRE + 256], *start = tc + LWS_PRE, esc[256]; lws_wsmsg_info_t info; int n; @@ -199,7 +200,8 @@ sais_eventchange(struct lws_ss_handle *hsrv, const char *event_uuid, int state) n = lws_snprintf(start, sizeof(tc) - LWS_PRE, "{\"schema\":\"sai-eventchange\", " "\"event_hash\":\"%s\", \"state\":%d}", - event_uuid, state); + lws_json_purify(esc, event_uuid, sizeof(esc) - 1, NULL), + state); memset(&info, 0, sizeof(info)); info.private_source_idx = SAI_WEBSRV_PB__GENERATED; diff --git a/src/server/s-ws-builder.c b/src/server/s-ws-builder.c index 60b8700..adb1e4c 100644 --- a/src/server/s-ws-builder.c +++ b/src/server/s-ws-builder.c @@ -1178,6 +1178,23 @@ sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b ap = (sai_artifact_t *)pss->a.dest; + /* + * The task_uuid from the builder decides which + * event db we open and reaches queries and + * broadcasts below, so it has to be a real + * server-minted task id, not something the + * builder cooked up. + */ + if (sais_validate_id(ap->task_uuid, + SAI_TASKID_LEN)) { + lwsl_wsi_err(pss->wsi, + "artifact upload with invalid " + "task_uuid"); + lwsac_free(&pss->a.ac); + + return -1; + } + sai_task_uuid_to_event_uuid(event_uuid, ap->task_uuid); /* @@ -1201,7 +1218,7 @@ sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b */ lws_sql_purify(esc, ap->task_uuid, sizeof(esc)); - lws_snprintf(s, sizeof(s)," and uuid == \"%s\"", esc); + lws_snprintf(s, sizeof(s)," and uuid == '%s'", esc); n = lws_struct_sq3_deserialize(pss->pdb_artifact, s, "run desc", lsm_schema_sq3_map_task, &o, &ac, 0, 1); @@ -1341,7 +1358,7 @@ sais_ws_json_rx_builder(struct vhd *vhd, struct pss *pss, uint8_t *buf, size_t b ap = (sai_artifact_t *)pss->a.dest; lws_sql_purify(esc, ap->task_uuid, sizeof(esc)); - lws_snprintf(s, sizeof(s)," select state from tasks where uuid == \"%s\" order by run desc limit 1", esc); + lws_snprintf(s, sizeof(s)," select state from tasks where uuid == '%s' order by run desc limit 1", esc); if (sqlite3_exec((sqlite3 *)pss->pdb_artifact, s, sql3_get_integer_cb, &state, NULL) != SQLITE_OK) { lwsl_err("%s: %s: %s: fail\n", __func__, s, diff --git a/src/server/s-ws-web.c b/src/server/s-ws-web.c index a9acebb..3416e5a 100644 --- a/src/server/s-ws-web.c +++ b/src/server/s-ws-web.c @@ -147,29 +147,6 @@ enum { }; static int -sais_validate_id(const char *id, int reqlen) -{ - const char *idin = id; - int n = reqlen; - - while (*id && n--) { - if (!((*id >= '0' && *id <= '9') || - (*id >= 'a' && *id <= 'z') || - (*id >= 'A' && *id <= 'Z'))) - goto reject; - id++; - } - - if (!n && !*id) - return 0; -reject: - - lwsl_notice("%s: Invalid ID (%d) '%s'\n", __func__, reqlen, idin); - - return 1; -} - -static int sais_validate_builder_name(const char *id) { const char *idin = id;