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 }