fix: harden platform form and response boundaries

This commit is contained in:
2026-07-27 12:19:34 +08:00
parent 34e8b092b3
commit cd58e1f6b6
7 changed files with 294 additions and 67 deletions

View File

@@ -126,7 +126,7 @@ func getResource(ctx *gin.Context, model any) {
}
func createResource(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) {
values, err := prepareResourceValues(ctx, allowedFields, relations)
values, err := prepareResourceValues(ctx, model, allowedFields, relations)
if err != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
@@ -161,41 +161,30 @@ func respondCreatedResource(ctx *gin.Context, value any) {
func maskCreatedSensitiveFields(value any) any {
switch data := value.(type) {
case map[string]any:
if phone, ok := data["phone"].(string); ok {
data["phone_masked"] = maskPhone(phone)
delete(data, "phone")
}
if realName, ok := data["real_name"].(string); ok {
data["real_name_masked"] = maskSecret(realName)
delete(data, "real_name")
}
if credentialNo, ok := data["credential_no"].(string); ok {
data["credential_no_masked"] = maskSecret(credentialNo)
delete(data, "credential_no")
}
delete(data, "longitude")
delete(data, "latitude")
safe := make(map[string]any)
for key, item := range data {
data[key] = maskCreatedSensitiveFields(item)
if isCreatedResponseField(key) {
safe[key] = maskCreatedSensitiveFields(item)
}
}
return safe
case []any:
for index := range data {
data[index] = maskCreatedSensitiveFields(data[index])
safe := make([]any, len(data))
for index, item := range data {
safe[index] = maskCreatedSensitiveFields(item)
}
return safe
}
return value
}
func maskSecret(value string) string {
characters := []rune(value)
if len(characters) <= 1 {
return "*"
func isCreatedResponseField(key string) bool {
switch key {
case "identity", "status", "version", "created_at", "updated_at":
return true
default:
return strings.HasSuffix(key, "_identity")
}
visible := 1
if len(characters) > 4 {
visible = 4
}
return strings.Repeat("*", len(characters)-visible) + string(characters[len(characters)-visible:])
}
func protectPreciseLocation(ctx *gin.Context, model, value any) any {
@@ -237,7 +226,7 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
return
}
values, err := resolveResourceRelations(input, allowedFields, relations, false)
if err != nil {
if err != nil || normalizeStringJSONBFields(model, values) != nil {
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
return
}
@@ -248,18 +237,48 @@ func updateResource(ctx *gin.Context, model any, allowedFields []string, relatio
updateAllowedByIdentity(ctx, model, values, append(allowedFields, relationColumns(relations)...))
}
func prepareResourceValues(ctx *gin.Context, allowedFields []string, relations []ResourceRelation) (map[string]any, error) {
func prepareResourceValues(ctx *gin.Context, model any, allowedFields []string, relations []ResourceRelation) (map[string]any, error) {
var input map[string]any
if err := ctx.ShouldBindJSON(&input); err != nil || len(input) == 0 {
return nil, errors.New("invalid resource payload")
}
values, err := resolveResourceRelations(input, allowedFields, relations, true)
if err != nil || len(values) == 0 {
if err != nil || normalizeStringJSONBFields(model, values) != nil || len(values) == 0 {
return nil, errors.New("invalid resource payload")
}
return values, nil
}
func normalizeStringJSONBFields(model any, values map[string]any) error {
modelType := reflect.TypeOf(model)
for modelType.Kind() == reflect.Pointer {
modelType = modelType.Elem()
}
for index := 0; index < modelType.NumField(); index++ {
field := modelType.Field(index)
if field.Type.Kind() != reflect.String || !strings.Contains(field.Tag.Get("gorm"), "type:jsonb") {
continue
}
column := gormColumn(field.Tag.Get("gorm"))
value, exists := values[column]
if !exists {
continue
}
if text, ok := value.(string); ok {
if !json.Valid([]byte(text)) {
return errors.New("invalid jsonb string")
}
continue
}
encoded, err := json.Marshal(value)
if err != nil || !json.Valid(encoded) {
return errors.New("invalid jsonb value")
}
values[column] = string(encoded)
}
return nil
}
func resolveResourceRelations(input map[string]any, allowedFields []string, relations []ResourceRelation, requireRelations bool) (map[string]any, error) {
values := filterFields(input, allowedFields)
for _, relation := range relations {