package validator import ( "encoding/json" "fmt" "math" "regexp" "strings" "unicode" "unicode/utf8" "browser.local/platform/domain" ) const ( maxGameClientBridgeArrayItems = 4096 maxGameClientBridgeIdentifierLength = 180 maxGameClientBridgeObjectKeys = 64 maxGameClientBridgePayloadDepth = 16 maxGameClientBridgePayloadNodes = 8192 maxGameClientBridgePayloadSize = 64 * 1024 maxGameClientBridgePayloadString = 16 * 1024 maxGameClientBridgeSessionLength = 4096 ) var ( gameClientBridgeAcronymBoundary = regexp.MustCompile(`([A-Z]+)([A-Z][a-z])`) gameClientBridgeCamelBoundary = regexp.MustCompile(`([a-z0-9])([A-Z])`) gameClientBridgeNonWord = regexp.MustCompile(`[^A-Za-z0-9]+`) ) func ValidateGameClientBridgeQueueRequest(request domain.GameClientBridgeQueueRequest) error { var violations []string violations = appendGameClientBridgeIdentifier(violations, "serverInstanceId", request.ServerInstanceID, true) violations = appendGameClientBridgeIdentifier(violations, "pluginId", request.PluginID, true) violations = appendGameClientBridgeIdentifier(violations, "profileKey", request.ProfileKey, true) violations = appendGameClientBridgeIdentifier(violations, "commandType", request.CommandType, true) violations = appendGameClientBridgeIdentifier(violations, "idempotencyKey", request.IdempotencyKey, true) if request.ExpiresAt.IsZero() { violations = append(violations, "expiresAt is required") } if request.Priority < 0 || request.Priority > 100 { violations = append(violations, "priority must be between 0 and 100") } violations = append(violations, validateGameClientBridgePayload(request.Payload)...) return finish(violations) } func ValidateGameClientBridgeClaimRequest(request domain.GameClientBridgeClaimRequest) error { var violations []string violations = appendGameClientBridgeSession(violations, request.SessionToken) if request.Limit < 0 || request.Limit > 50 { violations = append(violations, "limit must be between 0 and 50") } return finish(violations) } func ValidateGameClientBridgeAckRequest(request domain.GameClientBridgeAckRequest) error { var violations []string violations = appendGameClientBridgeSession(violations, request.SessionToken) violations = appendGameClientBridgeIdentifier(violations, "commandId", request.CommandID, true) if request.FencingToken == 0 { violations = append(violations, "fencingToken is required") } return finish(violations) } func ValidateGameClientBridgeResultRequest(request domain.GameClientBridgeResultRequest) error { var violations []string violations = appendGameClientBridgeSession(violations, request.SessionToken) violations = appendGameClientBridgeIdentifier(violations, "commandId", request.CommandID, true) if request.FencingToken == 0 { violations = append(violations, "fencingToken is required") } if request.Status != domain.GameClientBridgeResultSucceeded && request.Status != domain.GameClientBridgeResultFailed && request.Status != domain.GameClientBridgeResultUnknown && request.Status != domain.GameClientBridgeResultCancelled { violations = append(violations, "status is invalid") } violations = appendGameClientBridgeText(violations, "summary", request.Summary, 512) violations = append(violations, validateGameClientBridgePayload(request.Payload)...) return finish(violations) } func ValidateGameClientBridgeCancelRequest(request domain.GameClientBridgeCancelRequest) error { var violations []string violations = appendGameClientBridgeIdentifier(violations, "commandId", request.CommandID, true) violations = appendGameClientBridgeText(violations, "reason", request.Reason, 256) return finish(violations) } func ValidateGameClientBridgeSnapshotIngestRequest(request domain.GameClientBridgeSnapshotIngestRequest) error { var violations []string violations = appendGameClientBridgeSession(violations, request.SessionToken) violations = appendGameClientBridgeIdentifier(violations, "type", request.Type, true) violations = appendGameClientBridgeIdentifier(violations, "schemaVersion", request.SchemaVersion, true) violations = appendGameClientBridgeIdentifier(violations, "streamKey", request.StreamKey, true) if request.Sequence == 0 { violations = append(violations, "sequence must be positive") } if request.ObservedAt.IsZero() { violations = append(violations, "observedAt is required") } if request.Retention.KeepForSeconds <= 0 || request.Retention.KeepForSeconds > 31*24*60*60 { violations = append(violations, "retention.keepForSeconds must be between 1 and 2678400") } if request.Retention.MaxRecords < 0 || request.Retention.MaxRecords > 10000 { violations = append(violations, "retention.maxRecords must be between 0 and 10000") } if request.Payload == nil { violations = append(violations, "payload is required") } violations = append(violations, validateGameClientBridgePayload(request.Payload)...) return finish(violations) } func ValidateGameClientBridgeLogStreamRequest(request domain.GameClientBridgeLogStreamRequest) error { var violations []string violations = appendGameClientBridgeSession(violations, request.SessionToken) return finish(violations) } func ValidateGameClientBridgeSnapshotQuery(query domain.GameClientBridgeSnapshotQuery) error { var violations []string violations = appendGameClientBridgeIdentifier(violations, "serverInstanceId", query.ServerInstanceID, true) violations = appendGameClientBridgeIdentifier(violations, "pluginId", query.PluginID, true) violations = appendGameClientBridgeIdentifier(violations, "profileKey", query.ProfileKey, false) violations = appendGameClientBridgeIdentifier(violations, "type", query.Type, false) violations = appendGameClientBridgeIdentifier(violations, "streamKey", query.StreamKey, false) if query.Limit < 0 || query.Limit > 200 { violations = append(violations, "limit must be between 0 and 200") } return finish(violations) } func appendGameClientBridgeIdentifier(violations []string, field, value string, required bool) []string { if value == "" { if required { return append(violations, field+" is required") } return violations } if !utf8.ValidString(value) || strings.TrimSpace(value) != value || utf8.RuneCountInString(value) > maxGameClientBridgeIdentifierLength { return append(violations, field+" is invalid") } first, _ := utf8.DecodeRuneInString(value) last, _ := utf8.DecodeLastRuneInString(value) if !isGameClientBridgeASCIIAlphanumeric(first) || !isGameClientBridgeASCIIAlphanumeric(last) { return append(violations, field+" is invalid") } for _, character := range value { if !isGameClientBridgeIdentifierCharacter(character) { return append(violations, field+" is invalid") } } if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || strings.Contains(value, "://") || strings.Contains(value, "..") { return append(violations, field+" is invalid") } return violations } func isGameClientBridgeASCIIAlphanumeric(character rune) bool { return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' } func isGameClientBridgeIdentifierCharacter(character rune) bool { return character >= 'a' && character <= 'z' || character >= 'A' && character <= 'Z' || character >= '0' && character <= '9' || character == '.' || character == '_' || character == '-' || character == ':' } func appendGameClientBridgeSession(violations []string, value string) []string { if strings.TrimSpace(value) == "" { return append(violations, "sessionToken is required") } if !utf8.ValidString(value) || strings.TrimSpace(value) != value || len(value) > maxGameClientBridgeSessionLength || containsControlCharacter(value) { return append(violations, "sessionToken is invalid") } return violations } func appendGameClientBridgeText(violations []string, field, value string, maximum int) []string { if value == "" { return violations } if !utf8.ValidString(value) || strings.TrimSpace(value) != value || utf8.RuneCountInString(value) > maximum || containsControlCharacter(value) { violations = append(violations, field+" is invalid") } lowered := strings.ToLower(strings.TrimSpace(value)) if containsUnsafeRuntimeSecret(value) || looksLikeRawHostPath(value) || hasUnsafeGameClientBridgeReference(lowered) || containsEmbeddedGameClientBridgeHostPath(lowered) { violations = append(violations, field+" contains unsafe connection or host material") } for _, reason := range unsafePluginStringReasons(value) { violations = append(violations, field+": "+reason) } return violations } func containsControlCharacter(value string) bool { for _, character := range value { if unicode.IsControl(character) { return true } } return false } type gameClientBridgePayloadBudget struct { nodes int } func validateGameClientBridgePayload(payload map[string]any) []string { if payload == nil { return nil } budget := gameClientBridgePayloadBudget{} violations := validateGameClientBridgePayloadValue("payload", payload, 0, &budget) if len(violations) != 0 { return violations } encoded, err := json.Marshal(payload) if err != nil { return []string{"payload must be valid JSON"} } if len(encoded) > maxGameClientBridgePayloadSize { return []string{"payload is too large"} } return nil } func validateGameClientBridgePayloadValue(field string, value any, depth int, budget *gameClientBridgePayloadBudget) []string { budget.nodes++ if budget.nodes > maxGameClientBridgePayloadNodes { return []string{"payload has too many values"} } if depth > maxGameClientBridgePayloadDepth { return []string{"payload nesting is too deep"} } switch typed := value.(type) { case nil, bool: return nil case string: return validateGameClientBridgePayloadString(field, typed) case float64: if math.IsInf(typed, 0) || math.IsNaN(typed) { return []string{field + " must be a finite JSON number"} } return nil case float32: if math.IsInf(float64(typed), 0) || math.IsNaN(float64(typed)) { return []string{field + " must be a finite JSON number"} } return nil case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return nil case json.Number: if _, err := json.Marshal(typed); err != nil { return []string{field + " must be a valid JSON number"} } return nil case map[string]any: if len(typed) > maxGameClientBridgeObjectKeys { return []string{field + " has too many keys"} } var violations []string for key, item := range typed { if !validGameClientBridgePayloadKey(key) { violations = append(violations, field+" key "+fmt.Sprintf("%q", key)+" is invalid") continue } if unsafeGameClientBridgePayloadKey(key) { violations = append(violations, field+" contains forbidden key "+key) continue } violations = append(violations, validateGameClientBridgePayloadValue(field+"."+key, item, depth+1, budget)...) } return violations case []any: if len(typed) > maxGameClientBridgeArrayItems { return []string{field + " has too many items"} } var violations []string for index, item := range typed { violations = append(violations, validateGameClientBridgePayloadValue(fmt.Sprintf("%s[%d]", field, index), item, depth+1, budget)...) } return violations default: return []string{fmt.Sprintf("%s uses non-JSON type %T", field, value)} } } func validateGameClientBridgePayloadString(field, value string) []string { var violations []string if !utf8.ValidString(value) { violations = append(violations, field+" must be valid UTF-8") } if utf8.RuneCountInString(value) > maxGameClientBridgePayloadString { violations = append(violations, field+" is too long") } if containsControlCharacter(value) { violations = append(violations, field+" contains control characters") } for _, reason := range unsafePluginStringReasons(value) { violations = append(violations, field+": "+reason) } lowered := strings.ToLower(strings.TrimSpace(value)) if containsUnsafeRuntimeSecret(value) || hasUnsafeGameClientBridgeReference(lowered) || containsEmbeddedGameClientBridgeHostPath(lowered) { violations = append(violations, field+" contains unsafe connection material") } return violations } func validGameClientBridgePayloadKey(key string) bool { if key == "" || !utf8.ValidString(key) || strings.TrimSpace(key) != key || utf8.RuneCountInString(key) > 80 { return false } for _, character := range key { if !isGameClientBridgeIdentifierCharacter(character) || character == ':' { return false } } return true } func unsafeGameClientBridgePayloadKey(key string) bool { tokens := gameClientBridgePayloadKeyTokens(key) if len(tokens) == 0 { return true } normalized := strings.Join(tokens, "") for _, exact := range []string{ "absolutepath", "apikey", "commandline", "componentkey", "credential", "credentials", "directsocket", "dsn", "hostpath", "password", "passwd", "rawpath", "rawsql", "runendpoint", "runsocket", "script", "secret", "sessiontoken", "shell", "socket", "sql", "statement", "terminalcommand", } { if normalized == strings.ReplaceAll(exact, " ", "") { return true } } if last := tokens[len(tokens)-1]; last == "password" || last == "passwd" || last == "secret" || last == "credential" || last == "credentials" || last == "dsn" { return true } for _, sequence := range [][]string{ {"api", "key"}, {"access", "key"}, {"private", "key"}, {"auth", "token"}, {"access", "token"}, {"client", "secret"}, {"storage", "credential"}, {"component", "key"}, {"session", "token"}, {"host", "path"}, {"raw", "path"}, {"absolute", "path"}, {"file", "system", "path"}, {"direct", "socket"}, {"socket", "path"}, {"socket", "address"}, {"socket", "url"}, {"socket", "endpoint"}, {"run", "endpoint"}, {"run", "url"}, {"run", "socket"}, {"run", "token"}, {"run", "credential"}, {"raw", "sql"}, {"raw", "query"}, {"sql", "text"}, {"sql", "query"}, {"sql", "statement"}, {"arbitrary", "sql"}, {"shell", "command"}, {"shell", "script"}, {"script", "body"}, {"terminal", "command"}, {"command", "line"}, {"arbitrary", "shell"}, } { if gameClientBridgeContainsSensitiveSequence(tokens, sequence) { return true } } return false } func containsEmbeddedGameClientBridgeHostPath(value string) bool { for _, marker := range []string{"/etc/", "/var/", "/tmp/", "/home/", "/root/", "/private/", "/users/", "/volumes/", "/opt/", `:\\`} { if strings.Contains(value, marker) { return true } } return false } func gameClientBridgeContainsSensitiveSequence(tokens, sequence []string) bool { for start := 0; start+len(sequence) <= len(tokens); start++ { matched := true for index, expected := range sequence { if tokens[start+index] != expected { matched = false break } } if !matched { continue } end := start + len(sequence) if end == len(tokens) { return true } switch tokens[end] { case "address", "body", "content", "material", "path", "raw", "ref", "text", "url", "value": return true } } return false } func gameClientBridgePayloadKeyTokens(key string) []string { withAcronymBoundaries := gameClientBridgeAcronymBoundary.ReplaceAllString(key, `${1} ${2}`) withCamelBoundaries := gameClientBridgeCamelBoundary.ReplaceAllString(withAcronymBoundaries, `${1} ${2}`) return strings.Fields(strings.ToLower(gameClientBridgeNonWord.ReplaceAllString(withCamelBoundaries, " "))) } func hasUnsafeGameClientBridgeReference(value string) bool { for _, fragment := range []string{ "unix://", "tcp://", "mysql://", "postgres://", "postgresql://", "mongodb://", "redis://", "sqlite://", "sqlserver://", "mssql://", "odbc:", "secret://", "vault://", "env://", "http://127.", "https://127.", "http://localhost", "https://localhost", } { if strings.Contains(value, fragment) { return true } } return false }