fix(http): decode HTML entities in filenames in one pass (#1651)

unescapeHTMLEntities applied its replacements while ranging over a map,
so the order changed from run to run. An escaped ampersand such as
"a<b.txt" came out as "a<b.txt" or, when & was replaced
first, as the double-decoded "a<b.txt".

Replace all entities in a single pass with strings.NewReplacer and
cover the escaped-ampersand cases in TestUnescapeHTMLEntities, running
each case repeatedly so an order-dependent implementation fails.
This commit is contained in:
Chandra ObulaReddy authored and GitHub committed 2026-11-12 09:23:13 +08:00
1 parent 89dfbe4c45
commit 6cd95507fd
2 files changed
+28 -18

No files matched your search

+16 -3
View File
@@ -125,13 +125,26 @@ func TestUnescapeHTMLEntities(t *testing.T) {
in: "normal.txt",
want: "normal.txt",
},
{
name: "escaped ampersand before entity name is decoded once",
in: "a&amp;lt;b.txt",
want: "a&lt;b.txt",
},
{
name: "escaped ampersand before quote entity is decoded once",
in: "a&amp;quot;b.txt",
want: "a&quot;b.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := unescapeHTMLEntities(tt.in)
if got != tt.want {
t.Errorf("unescapeHTMLEntities() = %q, want %q", got, tt.want)
// Run repeatedly so that an order-dependent implementation fails reliably.
for i := 0; i < 200; i++ {
got := unescapeHTMLEntities(tt.in)
if got != tt.want {
t.Fatalf("unescapeHTMLEntities() = %q, want %q (run %d)", got, tt.want, i)
}
}
})
}
+12 -15
View File
@@ -408,24 +408,21 @@ func decodeFilenameParam(filename string) string {
return decoded
}
// htmlEntityReplacer replaces every entity in a single left-to-right pass, so an
// escaped ampersand such as "&amp;lt;" becomes "&lt;" and is not decoded twice.
var htmlEntityReplacer = strings.NewReplacer(
"&amp;", "&",
"&lt;", "<",
"&gt;", ">",
"&quot;", "\"",
"&#39;", "'",
"&apos;", "'",
)
// unescapeHTMLEntities unescapes common HTML entities in filenames
// This handles cases where servers HTML-encode special characters like & to &amp;
func unescapeHTMLEntities(s string) string {
// Common HTML entities that might appear in filenames
replacements := map[string]string{
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&quot;": "\"",
"&#39;": "'",
"&apos;": "'",
}
result := s
for entity, char := range replacements {
result = strings.ReplaceAll(result, entity, char)
}
return result
return htmlEntityReplacer.Replace(s)
}
// tryDecodeGBK attempts to decode string as GBK encoding