114 lines
3.7 KiB
Go
114 lines
3.7 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"unicode/utf8"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
const jsonBodyMaxBytes = 1024 * 1024
|
|
|
|
var errJSONBodyTooLarge = errors.New("тело JSON превышает 1 МиБ")
|
|
|
|
// strictBindJSON не позволяет стандартному encoding/json молча заменить
|
|
// повреждённый UTF-8 или непарный UTF-16 surrogate escape на U+FFFD.
|
|
//
|
|
// Это критично для секретов: U+FFFD сам по себе разрешён, поэтому без этой
|
|
// границы другое байтовое значение могло аутентифицироваться как пароль,
|
|
// который фактически хранится в базе. После проверки тело восстанавливается и
|
|
// передаётся штатному Gin binder, чтобы сохранить действующий API-контракт.
|
|
func strictBindJSON(c *gin.Context, dst any) error {
|
|
body := c.Request.Body
|
|
if body == nil {
|
|
return errors.New("пустое тело JSON")
|
|
}
|
|
raw, err := io.ReadAll(io.LimitReader(body, jsonBodyMaxBytes+1))
|
|
_ = body.Close()
|
|
c.Request.Body = io.NopCloser(bytes.NewReader(raw))
|
|
if err != nil {
|
|
return fmt.Errorf("не удалось прочитать тело JSON: %w", err)
|
|
}
|
|
if len(raw) > jsonBodyMaxBytes {
|
|
return errJSONBodyTooLarge
|
|
}
|
|
if err := validateStrictJSONEncoding(raw); err != nil {
|
|
return err
|
|
}
|
|
return c.ShouldBindJSON(dst)
|
|
}
|
|
|
|
// validateStrictJSONEncoding проверяет только границу кодировки. Синтаксис,
|
|
// типы и структуру затем проверяет encoding/json; здесь отдельно закрывается
|
|
// его документированное replacement-поведение.
|
|
func validateStrictJSONEncoding(raw []byte) error {
|
|
if !utf8.Valid(raw) {
|
|
return errors.New("тело JSON содержит некорректный UTF-8")
|
|
}
|
|
|
|
inString := false
|
|
for i := 0; i < len(raw); i++ {
|
|
switch raw[i] {
|
|
case '"':
|
|
inString = !inString
|
|
case '\\':
|
|
if !inString {
|
|
continue
|
|
}
|
|
i++
|
|
if i >= len(raw) {
|
|
return errors.New("незавершённая escape-последовательность JSON")
|
|
}
|
|
if raw[i] != 'u' {
|
|
continue
|
|
}
|
|
|
|
code, ok := parseJSONHex4(raw, i+1)
|
|
if !ok {
|
|
return errors.New("некорректная Unicode escape-последовательность JSON")
|
|
}
|
|
i += 4
|
|
switch {
|
|
case code >= 0xD800 && code <= 0xDBFF:
|
|
// Старший суррогат допустим только как первая половина пары и
|
|
// только в соседней escape-последовательности \uXXXX.
|
|
if i+6 >= len(raw) || raw[i+1] != '\\' || raw[i+2] != 'u' {
|
|
return errors.New("непарный старший UTF-16 суррогат в JSON")
|
|
}
|
|
low, lowOK := parseJSONHex4(raw, i+3)
|
|
if !lowOK || low < 0xDC00 || low > 0xDFFF {
|
|
return errors.New("непарный старший UTF-16 суррогат в JSON")
|
|
}
|
|
i += 6
|
|
case code >= 0xDC00 && code <= 0xDFFF:
|
|
return errors.New("непарный младший UTF-16 суррогат в JSON")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func parseJSONHex4(raw []byte, start int) (uint16, bool) {
|
|
if start < 0 || start+4 > len(raw) {
|
|
return 0, false
|
|
}
|
|
var value uint16
|
|
for _, character := range raw[start : start+4] {
|
|
value <<= 4
|
|
switch {
|
|
case character >= '0' && character <= '9':
|
|
value += uint16(character - '0')
|
|
case character >= 'a' && character <= 'f':
|
|
value += uint16(character-'a') + 10
|
|
case character >= 'A' && character <= 'F':
|
|
value += uint16(character-'A') + 10
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
return value, true
|
|
}
|