Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
100
backend/internal/htmlsanitize/htmlsanitize.go
Normal file
100
backend/internal/htmlsanitize/htmlsanitize.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
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] == '['
|
||||
}
|
||||
55
backend/internal/htmlsanitize/htmlsanitize_test.go
Normal file
55
backend/internal/htmlsanitize/htmlsanitize_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package htmlsanitize
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"html"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestJSONBodyEscapesStringValues(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got, ok := JSONBody([]byte(`{"title":"<script>alert(1)</script>","items":["safe & sound",{"description":"<b>mode</b>"}],"count":1}`))
|
||||
if !ok {
|
||||
t.Fatal("JSONBody() ok = false, want true")
|
||||
}
|
||||
|
||||
var body map[string]any
|
||||
if errUnmarshal := json.Unmarshal(got, &body); errUnmarshal != nil {
|
||||
t.Fatalf("Unmarshal() error = %v; body=%s", errUnmarshal, string(got))
|
||||
}
|
||||
if body["title"] != html.EscapeString("<script>alert(1)</script>") {
|
||||
t.Fatalf("title = %q, want escaped", body["title"])
|
||||
}
|
||||
items, okItems := body["items"].([]any)
|
||||
if !okItems || len(items) != 2 {
|
||||
t.Fatalf("items = %#v, want two items", body["items"])
|
||||
}
|
||||
if items[0] != html.EscapeString("safe & sound") {
|
||||
t.Fatalf("items[0] = %q, want escaped", items[0])
|
||||
}
|
||||
nested, okNested := items[1].(map[string]any)
|
||||
if !okNested {
|
||||
t.Fatalf("items[1] = %#v, want object", items[1])
|
||||
}
|
||||
if nested["description"] != html.EscapeString("<b>mode</b>") {
|
||||
t.Fatalf("description = %q, want escaped", nested["description"])
|
||||
}
|
||||
if body["count"] != float64(1) {
|
||||
t.Fatalf("count = %#v, want unchanged number", body["count"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONBodyIfLikelySkipsNonJSONHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
body := []byte("<!doctype html><title>plugin</title>")
|
||||
got, ok := JSONBodyIfLikely(body, "text/html; charset=utf-8")
|
||||
if ok {
|
||||
t.Fatal("JSONBodyIfLikely() ok = true, want false")
|
||||
}
|
||||
if !bytes.Equal(got, body) {
|
||||
t.Fatalf("body = %q, want unchanged %q", string(got), string(body))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue