fix: escape variant names, single-pass StripTags, WASM security contract
- Escape variant name in Responsive.Render HTML attribute (XSS fix) - Rewrite StripTags to single-pass O(n) space collapsing - Document Raw() security contract in WASM entry point - Add TestAttr_NonElement coverage - Fix Makefile WASM target to rebuild on source changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
18d2933315
commit
9bc1fa7c69
5 changed files with 31 additions and 11 deletions
2
Makefile
2
Makefile
|
|
@ -11,7 +11,7 @@ test:
|
||||||
|
|
||||||
wasm: $(WASM_OUT)
|
wasm: $(WASM_OUT)
|
||||||
|
|
||||||
$(WASM_OUT):
|
$(WASM_OUT): $(shell find . -name '*.go' -not -path './dist/*')
|
||||||
@mkdir -p dist
|
@mkdir -p dist
|
||||||
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o $(WASM_OUT) ./cmd/wasm/
|
GOOS=js GOARCH=wasm go build -ldflags="-s -w" -o $(WASM_OUT) ./cmd/wasm/
|
||||||
@RAW=$$(stat -c%s "$(WASM_OUT)" 2>/dev/null || stat -f%z "$(WASM_OUT)"); \
|
@RAW=$$(stat -c%s "$(WASM_OUT)" 2>/dev/null || stat -f%z "$(WASM_OUT)"); \
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ import (
|
||||||
html "forge.lthn.ai/core/go-html"
|
html "forge.lthn.ai/core/go-html"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// renderToString builds an HLCRF layout from JS arguments and returns HTML.
|
||||||
|
// Slot content is injected via Raw() — the caller is responsible for sanitisation.
|
||||||
|
// This is intentional: the WASM module is a rendering engine for trusted content
|
||||||
|
// produced server-side or by the application's own templates.
|
||||||
func renderToString(_ js.Value, args []js.Value) any {
|
func renderToString(_ js.Value, args []js.Value) any {
|
||||||
if len(args) < 1 {
|
if len(args) < 1 {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
|
|
@ -183,6 +183,14 @@ func TestElNode_MultipleAttrs(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAttr_NonElement(t *testing.T) {
|
||||||
|
node := Attr(Raw("text"), "class", "x")
|
||||||
|
got := node.Render(NewContext())
|
||||||
|
if got != "text" {
|
||||||
|
t.Errorf("Attr on non-element should return unchanged, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSwitchNode(t *testing.T) {
|
func TestSwitchNode(t *testing.T) {
|
||||||
ctx := NewContext()
|
ctx := NewContext()
|
||||||
cases := map[string]Node{
|
cases := map[string]Node{
|
||||||
|
|
|
||||||
26
pipeline.go
26
pipeline.go
|
|
@ -7,30 +7,38 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// StripTags removes HTML tags from rendered output, returning plain text.
|
// StripTags removes HTML tags from rendered output, returning plain text.
|
||||||
// Tag boundaries are replaced with a single space; result is trimmed.
|
// Tag boundaries are collapsed into single spaces; result is trimmed.
|
||||||
|
// Does not handle script/style element content (go-html does not generate these).
|
||||||
func StripTags(html string) string {
|
func StripTags(html string) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
inTag := false
|
inTag := false
|
||||||
|
prevSpace := true // starts true to trim leading space
|
||||||
for _, r := range html {
|
for _, r := range html {
|
||||||
if r == '<' {
|
if r == '<' {
|
||||||
inTag = true
|
inTag = true
|
||||||
b.WriteByte(' ')
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if r == '>' {
|
if r == '>' {
|
||||||
inTag = false
|
inTag = false
|
||||||
|
if !prevSpace {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
prevSpace = true
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !inTag {
|
if !inTag {
|
||||||
b.WriteRune(r)
|
if r == ' ' || r == '\t' || r == '\n' {
|
||||||
|
if !prevSpace {
|
||||||
|
b.WriteByte(' ')
|
||||||
|
prevSpace = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b.WriteRune(r)
|
||||||
|
prevSpace = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Collapse multiple spaces into one.
|
return strings.TrimSpace(b.String())
|
||||||
result := b.String()
|
|
||||||
for strings.Contains(result, " ") {
|
|
||||||
result = strings.ReplaceAll(result, " ", " ")
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Imprint renders a node tree to HTML, strips tags, tokenises the text,
|
// Imprint renders a node tree to HTML, strips tags, tokenises the text,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ func (r *Responsive) Render(ctx *Context) string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
for _, v := range r.variants {
|
for _, v := range r.variants {
|
||||||
b.WriteString(`<div data-variant="`)
|
b.WriteString(`<div data-variant="`)
|
||||||
b.WriteString(v.name)
|
b.WriteString(escapeAttr(v.name))
|
||||||
b.WriteString(`">`)
|
b.WriteString(`">`)
|
||||||
b.WriteString(v.layout.Render(ctx))
|
b.WriteString(v.layout.Render(ctx))
|
||||||
b.WriteString(`</div>`)
|
b.WriteString(`</div>`)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue