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:
@@ -15,6 +15,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -108,6 +109,32 @@ func generateMQTTPassword() (string, error) {
|
|||||||
return "", lastErr
|
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.
|
// CreateIoTDevice — POST /v1/namespaces/{ns}/iot/devices.
|
||||||
func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) CreateIoTDevice(w http.ResponseWriter, r *http.Request) {
|
||||||
ns := pathVar(r, "namespace")
|
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()))
|
writeJSON(w, http.StatusBadRequest, errResp("invalid JSON: "+err.Error()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.Name == "" {
|
// Валидация идентификаторов до записи в БД: единый charset и лимиты для
|
||||||
writeJSON(w, http.StatusBadRequest, errResp("name is required"))
|
// 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
|
return
|
||||||
}
|
}
|
||||||
if req.DeviceID == "" {
|
if msg := validateIdentifier(req.Name, "name", 64); msg != "" {
|
||||||
writeJSON(w, http.StatusBadRequest, errResp("device_id is required"))
|
writeJSON(w, http.StatusBadRequest, errResp(msg))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if msg := validateIdentifier(req.DeviceID, "device_id", 64); msg != "" {
|
||||||
|
writeJSON(w, http.StatusBadRequest, errResp(msg))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user