| Author | Andy Green <andy@warmcat.com> 2026-09-06 07:53 UTC | | Committer | Andy Green <andy@warmcat.com> 2026-09-06 07:53 UTC | | Tree | 6ca24bca719789ff897255b177b552e20e8b44fb 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;
|