Project homepage Mailing List  Warmcat.com  API Docs  Github Mirror 
    npro  
 Modern all-safe Rust Network Protocol library supporting h1, h2, h3, ws, wt sans-IO and with socket IO + tls
git clone https://npro.rs/repo/npro
 
root / scripts / sai-power.service
Author[]Andy Green <andy@warmcat.com> 2026-09-06 07:53 UTC
Committer[]Andy Green <andy@warmcat.com> 2026-09-06 07:53 UTC
Tree6ca24bca719789ff897255b177b552e20e8b44fb   Raw Patch
 
server: validate builder artifact task_uuid, escape taskchange JSON, fixes F-011
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;
Page fetched 0s ago, creation time: 4ms (vhost etag hits: 0%, cache hits: 0%)