From 8335afb6170b4ae6df6c61b4193260cbc0aeb06d Mon Sep 17 00:00:00 2001 From: Repinoid Date: Sat, 10 Oct 2026 18:50:56 +0300 Subject: [PATCH] =?UTF-8?q?fix(iot):=20=D0=B2=D0=B0=D0=BB=D0=B8=D0=B4?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D1=8F=20namespace/name/device=5Fid=20(=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D1=80=D0=B5=D1=82=20=5F,=20+,=20#,=20/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен identRe ^[a-zA-Z0-9][a-zA-Z0-9-]*$ и validateIdentifier. - CreateIoTDevice теперь проверяет namespace (≤56), name (≤64), device_id (≤64). - Причины: _ ломает username «namespace_device_id» (auth EMQX LIMIT 1 без ORDER BY), +/# — wildcard в ACL-топике, / — лишний уровень топика. --- internal/service/api/handler/devices.go | 43 ++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/internal/service/api/handler/devices.go b/internal/service/api/handler/devices.go index 1936689..703a0a0 100644 --- a/internal/service/api/handler/devices.go +++ b/internal/service/api/handler/devices.go @@ -15,6 +15,7 @@ import ( "encoding/json" "errors" "net/http" + "regexp" "strconv" "time" @@ -108,6 +109,32 @@ func generateMQTTPassword() (string, error) { return "", lastErr } +// identRe — допустимый charset идентификаторов (namespace/name/device_id): +// только [a-zA-Z0-9] и дефисы, не с дефиса. Исключает _, +, #, /, пробелы, юникод. +// Причины запретов (ревью 2026-10-10): +// - _ — username = «namespace_device_id»; _ делает его неоднозначным +// (auth-запрос EMQX: namespace || '_' || device_id, LIMIT 1 без ORDER BY +// → может сматчить чужое устройство); +// - +/# — wildcard MQTT: в ACL-топике даст подписку/публикацию чужих топиков; +// - / — ломает топик «ns/telemetry/device» (лишний уровень). +var identRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]*$`) + +// validateIdentifier возвращает текст ошибки (или "") для идентификатора value. +// what — имя поля для сообщения, maxLen — максимальная длина (namespace ≤ 56: +// имя БД tenant_ усекается PG до 63 байт). +func validateIdentifier(value, what string, maxLen int) string { + if value == "" { + return what + " is required" + } + if len(value) > maxLen { + return what + " is too long (max " + strconv.Itoa(maxLen) + " chars)" + } + if !identRe.MatchString(value) { + return what + " must match [a-zA-Z0-9][a-zA-Z0-9-]* (no _, +, #, /, spaces)" + } + return "" +} + // CreateIoTDevice — POST /v1/namespaces/{ns}/iot/devices. func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) { ns := pathVar(r, "namespace") @@ -120,12 +147,20 @@ func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error())) return } - if req.Name == "" { - writeJSON(w, http.StatusBadRequest, errResp("name is required")) + // Валидация идентификаторов до записи в БД: единый charset и лимиты для + // namespace (из URL), name и device_id. Критично для MQTT (username/топик) + // и PG (имя БД tenant_): плохие символы ломают auth/ACL или дают чужой + // доступ (см. identRe). Ошибка 400 с понятным текстом — для Terraform. + if msg := validateIdentifier(ns, "namespace", 56); msg != "" { + writeJSON(w, http.StatusBadRequest, errResp(msg)) return } - if req.DeviceID == "" { - writeJSON(w, http.StatusBadRequest, errResp("device_id is required")) + if msg := validateIdentifier(req.Name, "name", 64); msg != "" { + writeJSON(w, http.StatusBadRequest, errResp(msg)) + return + } + if msg := validateIdentifier(req.DeviceID, "device_id", 64); msg != "" { + writeJSON(w, http.StatusBadRequest, errResp(msg)) return }