fix(iot): валидация namespace/name/device_id (запрет _, +, #, /)

- Добавлен 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-топике, / — лишний уровень топика.
This commit is contained in:
Repinoid
2026-10-10 18:50:56 +03:00
parent 2aef884d02
commit 8335afb617
+39 -4
View File
@@ -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_<ns> усекается 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_<ns>): плохие символы ломают 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
}