From f5e7599749ce47b2a65d172497c94851509b32dd Mon Sep 17 00:00:00 2001 From: simon chrzanowski Date: Wed, 14 Oct 2026 08:37:40 +0200 Subject: [PATCH] Stop interpolating archive paths into inline handlers The source browser built its file tree with onclick handlers such as loadFile('${escapeHTML(file.path)}'). The browser decodes HTML entities in an attribute before running the handler, so the ' that escapeHTML produces turns back into a quote and ends the string. A cached package with a crafted file name ran script on the proxy origin for anyone who browsed it. - Carry the path in data-path and dispatch clicks from one delegated listener on the file tree - Render the binary download button as a link instead of an onclick that assigns window.location - Fail the template tests when an inline handler interpolates data --- .../server/templates/pages/browse_source.html | 27 ++++++++++++---- internal/server/templates_handlers_test.go | 32 +++++++++++++++++++ 2 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 internal/server/templates_handlers_test.go diff --git a/internal/server/templates/pages/browse_source.html b/internal/server/templates/pages/browse_source.html index a949111a..0d5332a2 100644 --- a/internal/server/templates/pages/browse_source.html +++ b/internal/server/templates/pages/browse_source.html @@ -104,7 +104,7 @@

Select a file

const parentPath = basePath.split('/').slice(0, -2).join('/'); html += `
+ data-action="open-dir" data-path="${escapeHTML(parentPath)}"> ..
`; @@ -119,13 +119,13 @@

Select a file

if (file.is_dir) { html += ` -
+
${iconHTML}${escapeHTML(file.name)}
`; } else { html += ` -
+
${iconHTML}${escapeHTML(file.name)} ${formatSize(file.size)}
@@ -179,10 +179,10 @@

Select a file

container.innerHTML = `

Binary file (${formatSize(content.length)})

- +
`; } @@ -207,6 +207,21 @@

Select a file

return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } +// Entries carry their path in data-path rather than in inline handlers: the +// browser decodes entities in a handler attribute before running it, so an +// escaped quote in a file name would end the handler's string. +document.getElementById('file-tree').addEventListener('click', (event) => { + const entry = event.target.closest('[data-action]'); + if (!entry) return; + const path = entry.dataset.path; + if (entry.dataset.action === 'open-dir') { + loadFileTree(path); + currentPath = path; + } else if (entry.dataset.action === 'open-file') { + loadFile(path); + } +}); + // Load root directory on page load loadFileTree(); diff --git a/internal/server/templates_handlers_test.go b/internal/server/templates_handlers_test.go new file mode 100644 index 00000000..1e3f1f21 --- /dev/null +++ b/internal/server/templates_handlers_test.go @@ -0,0 +1,32 @@ +package server + +import ( + "io/fs" + "regexp" + "testing" +) + +// inlineHandlerWithData matches an inline event handler attribute whose value +// interpolates a JavaScript template expression. The browser decodes HTML +// entities in the attribute before running the handler, so escapeHTML cannot +// keep a quote in the interpolated value from ending the handler's string. +var inlineHandlerWithData = regexp.MustCompile(`\son[a-z]+\s*=\s*"[^"]*\$\{`) + +func TestTemplatesInterpolateNoDataIntoInlineHandlers(t *testing.T) { + err := fs.WalkDir(templatesFS, "templates", func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + source, err := fs.ReadFile(templatesFS, path) + if err != nil { + return err + } + for _, match := range inlineHandlerWithData.FindAll(source, -1) { + t.Errorf("%s: inline handler interpolates data: %s", path, match) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +}