Skip to content

Commit 5d7785f

Browse files
authoredMar 23, 2025··
fix: incorrect parsing of HTML within JavaScript strings, fixes #1106 (#1107)
1 parent 9ecbc32 commit 5d7785f

File tree

9 files changed

+113
-3
lines changed

9 files changed

+113
-3
lines changed
 

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ result
2323
# Editors
2424
## nvim
2525
.null-ls*
26+
# vscode
27+
.vscode/
2628

2729
# Go workspace.
2830
go.work

‎parser/v2/fuzz.sh

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
#!/bin/bash
12
echo Element
23
go test -fuzz=FuzzElement -fuzztime=120s
4+
echo Script
5+
go test -fuzz=FuzzScript -fuzztime=120s

‎parser/v2/goexpression/fuzz.sh

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#!/bin/bash
12
echo If
23
go test -fuzz=FuzzIf -fuzztime=120s
34
echo For

‎parser/v2/scriptparser.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ func (p scriptElementParser) Parse(pi *parse.Input) (n Node, ok bool, err error)
5252
// Parse the contents, we should get script text or Go expressions up until the closing tag.
5353
var sb strings.Builder
5454
var isInsideStringLiteral bool
55+
5556
loop:
5657
for {
5758
// Read and decide whether we're we've hit a:
@@ -70,6 +71,11 @@ loop:
7071
break loop
7172
}
7273

74+
if _, ok, err = endTagStart.Parse(pi); err != nil || ok {
75+
// We've reached the end of the script, but the end tag is probably invalid.
76+
break loop
77+
}
78+
7379
var code Node
7480
code, ok, err = goCodeInJavaScript.Parse(pi)
7581
if err != nil {
@@ -91,6 +97,7 @@ loop:
9197
continue loop
9298
}
9399

100+
// Read JavaScript chracaters.
94101
for {
95102
before := pi.Index()
96103
var c string
@@ -105,7 +112,11 @@ loop:
105112
}
106113
peeked, _ := pi.Peek(1)
107114
peeked = c + peeked
108-
if isEOF || peeked == "{{" || peeked == "</" || peeked == "//" || peeked == "/*" {
115+
116+
breakForGo := peeked == "{{"
117+
breakForHTML := !isInsideStringLiteral && (peeked == "</" || peeked == "//" || peeked == "/*")
118+
119+
if isEOF || breakForGo || breakForHTML {
109120
if sb.Len() > 0 {
110121
e.Contents = append(e.Contents, NewScriptContentsJS(sb.String()))
111122
sb.Reset()
@@ -128,6 +139,7 @@ loop:
128139
}
129140

130141
var jsEndTag = parse.String("</script>")
142+
var endTagStart = parse.String("</")
131143

132144
var jsCharacter = parse.Any(jsEscapedCharacter, parse.AnyRune)
133145

@@ -136,7 +148,9 @@ var jsEscapedCharacter = parse.StringFrom(parse.String("\\"), parse.AnyRune)
136148
var jsComment = parse.Any(jsSingleLineComment, jsMultiLineComment)
137149

138150
var jsStartSingleLineComment = parse.String("//")
139-
var jsSingleLineComment = parse.StringFrom(jsStartSingleLineComment, parse.StringUntil(parse.NewLine), parse.NewLine)
151+
var jsEndOfSingleLineComment = parse.StringFrom(parse.Or(parse.NewLine, parse.EOF[string]()))
152+
var jsSingleLineComment = parse.StringFrom(jsStartSingleLineComment, parse.StringUntil(jsEndOfSingleLineComment), jsEndOfSingleLineComment)
140153

141154
var jsStartMultiLineComment = parse.String("/*")
142-
var jsMultiLineComment = parse.StringFrom(jsStartMultiLineComment, parse.StringUntil(parse.String("*/")), parse.String("*/"), parse.OptionalWhitespace)
155+
var jsEndOfMultiLineComment = parse.StringFrom(parse.Or(parse.String("*/"), parse.EOF[string]()))
156+
var jsMultiLineComment = parse.StringFrom(jsStartMultiLineComment, parse.StringUntil(jsEndOfMultiLineComment), jsEndOfMultiLineComment, parse.OptionalWhitespace)

‎parser/v2/scriptparser_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,55 @@
11
package parser
22

33
import (
4+
"path/filepath"
45
"testing"
56

7+
_ "embed"
8+
69
"github.com/a-h/parse"
710
"github.com/google/go-cmp/cmp"
11+
"golang.org/x/tools/txtar"
812
)
913

14+
func TestScriptElementParserPlain(t *testing.T) {
15+
files, _ := filepath.Glob("scriptparsertestdata/*.txt")
16+
if len(files) == 0 {
17+
t.Errorf("no test files found")
18+
}
19+
for _, file := range files {
20+
t.Run(filepath.Base(file), func(t *testing.T) {
21+
a, err := txtar.ParseFile(file)
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
if len(a.Files) != 2 {
26+
t.Fatalf("expected 2 files, got %d", len(a.Files))
27+
}
28+
29+
input := parse.NewInput(clean(a.Files[0].Data))
30+
result, ok, err := scriptElement.Parse(input)
31+
if err != nil {
32+
t.Fatalf("parser error: %v", err)
33+
}
34+
if !ok {
35+
t.Fatalf("failed to parse at %d", input.Index())
36+
}
37+
38+
se, isScriptElement := result.(ScriptElement)
39+
if !isScriptElement {
40+
t.Fatalf("expected ScriptElement, got %T", result)
41+
}
42+
if len(se.Contents) != 1 {
43+
t.Fatalf("expected 1 content, got %d", len(se.Contents))
44+
}
45+
expected := clean(a.Files[1].Data)
46+
if diff := cmp.Diff(*se.Contents[0].Value, string(expected)); diff != "" {
47+
t.Fatalf("%s:\n%s", file, diff)
48+
}
49+
})
50+
}
51+
}
52+
1053
func TestScriptElementParser(t *testing.T) {
1154
tests := []struct {
1255
name string
@@ -198,3 +241,24 @@ but it's commented out */
198241
})
199242
}
200243
}
244+
245+
func FuzzScriptParser(f *testing.F) {
246+
files, _ := filepath.Glob("scriptparsertestdata/*.txt")
247+
if len(files) == 0 {
248+
f.Errorf("no test files found")
249+
}
250+
for _, file := range files {
251+
a, err := txtar.ParseFile(file)
252+
if err != nil {
253+
f.Fatal(err)
254+
}
255+
if len(a.Files) != 2 {
256+
f.Fatalf("expected 2 files, got %d", len(a.Files))
257+
}
258+
f.Add(clean(a.Files[0].Data))
259+
}
260+
261+
f.Fuzz(func(t *testing.T, input string) {
262+
_, _, _ = scriptElement.Parse(parse.NewInput(input))
263+
})
264+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
-- in --
2+
<script>
3+
function showSuccessMessage(responseText) {
4+
const formResponse = document.getElementById('form-response');
5+
formResponse.innerHTML = `
6+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
7+
${responseText}
8+
</div>`;
9+
}
10+
</script>
11+
-- out --
12+
13+
function showSuccessMessage(responseText) {
14+
const formResponse = document.getElementById('form-response');
15+
formResponse.innerHTML = `
16+
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded">
17+
${responseText}
18+
</div>`;
19+
}
20+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("<script>/*/00000\x17I020000000////00000\x17000")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("<script>00000000000000000000////0000000000")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
go test fuzz v1
2+
string("<script>\n function showSuccessMessage(responseText) {\n\t\t\tconst formRespo</scriptcument.getElementById('form-response');\n\t\t\tformResponse.innerHTML = `\n\t\t\t\t<div class=\"bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded\">\n\t\t\t\t\t${responseText}\n\t\t\t\t</div>`;\n\t\t}\nnse = do>")

0 commit comments

Comments
 (0)
Please sign in to comment.