mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-22 22:36:34 +08:00
- Introduced `htmlsanitize` package for escaping HTML and handling JSON body sanitization to prevent XSS vulnerabilities. - Integrated sanitization functions into plugin store, plugin host, and API management handlers to ensure all user-facing content is escaped. - Added unit tests to verify proper escaping of HTML strings, JSON bodies, and nested data structures. - Updated existing management and plugin-related tests to validate sanitization implementations.
101 lines
2.5 KiB
Go
101 lines
2.5 KiB
Go
package htmlsanitize
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"html"
|
|
"io"
|
|
"mime"
|
|
"strings"
|
|
)
|
|
|
|
// String escapes text before it is returned to browser-facing management clients.
|
|
func String(value string) string {
|
|
return html.EscapeString(value)
|
|
}
|
|
|
|
// Strings escapes each string in values while preserving order.
|
|
func Strings(values []string) []string {
|
|
out := make([]string, 0, len(values))
|
|
for _, value := range values {
|
|
out = append(out, String(value))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// JSONBody escapes all string values in a JSON document.
|
|
func JSONBody(body []byte) ([]byte, bool) {
|
|
trimmed := bytes.TrimSpace(body)
|
|
if len(trimmed) == 0 {
|
|
return body, false
|
|
}
|
|
|
|
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
|
decoder.UseNumber()
|
|
var value any
|
|
if errDecode := decoder.Decode(&value); errDecode != nil {
|
|
return body, false
|
|
}
|
|
var extra any
|
|
if errExtra := decoder.Decode(&extra); errExtra != io.EOF {
|
|
return body, false
|
|
}
|
|
|
|
var buffer bytes.Buffer
|
|
encoder := json.NewEncoder(&buffer)
|
|
encoder.SetEscapeHTML(false)
|
|
if errEncode := encoder.Encode(JSONValue(value)); errEncode != nil {
|
|
return body, false
|
|
}
|
|
return bytes.TrimSuffix(buffer.Bytes(), []byte("\n")), true
|
|
}
|
|
|
|
// JSONBodyIfLikely escapes JSON bodies when the content type or body shape indicates JSON.
|
|
func JSONBodyIfLikely(body []byte, contentType string) ([]byte, bool) {
|
|
if IsJSONContentType(contentType) || LooksLikeJSON(body) {
|
|
return JSONBody(body)
|
|
}
|
|
return body, false
|
|
}
|
|
|
|
// JSONValue recursively escapes string values in JSON-compatible data.
|
|
func JSONValue(value any) any {
|
|
switch typed := value.(type) {
|
|
case string:
|
|
return String(typed)
|
|
case []any:
|
|
out := make([]any, len(typed))
|
|
for index, item := range typed {
|
|
out[index] = JSONValue(item)
|
|
}
|
|
return out
|
|
case map[string]any:
|
|
out := make(map[string]any, len(typed))
|
|
for key, item := range typed {
|
|
out[key] = JSONValue(item)
|
|
}
|
|
return out
|
|
default:
|
|
return value
|
|
}
|
|
}
|
|
|
|
// IsJSONContentType reports whether contentType is application/json or a +json type.
|
|
func IsJSONContentType(contentType string) bool {
|
|
mediaType, _, errParse := mime.ParseMediaType(strings.TrimSpace(contentType))
|
|
if errParse != nil {
|
|
mediaType = strings.TrimSpace(contentType)
|
|
}
|
|
mediaType = strings.ToLower(mediaType)
|
|
return mediaType == "application/json" || strings.HasSuffix(mediaType, "+json")
|
|
}
|
|
|
|
// LooksLikeJSON reports whether body starts with an object or array JSON marker.
|
|
func LooksLikeJSON(body []byte) bool {
|
|
trimmed := bytes.TrimSpace(body)
|
|
if len(trimmed) == 0 {
|
|
return false
|
|
}
|
|
return trimmed[0] == '{' || trimmed[0] == '['
|
|
}
|