Problem
The proxy variant of the tracking script (dist/latest/custom/proxy.js, built from src/default.js with the {{nginxProxyHost}} placeholder) ends with }(window,{},"<base url>"), where the base URL is filled in by the web server at request time from the hostname and path query parameters. The value lands directly inside a double-quoted JavaScript string literal with no escaping, so any substituted value containing a quote or backslash breaks out of the literal and executes as script on the domain serving the file — a customer-controlled proxy domain.
On the old nginx custom-domains server this is not reachable, because nginx's $arg_hostname and $arg_path return the raw, still percent-encoded query value: a request for ?hostname=%22%2balert(1)%2b%22 emits the literal text %22%2balert(1)%2b%22 and the string literal stays intact.
The Caddy custom-domains setup evaluates the same placeholders through Caddy's templates directive, which uses Go's text/template and applies no contextual escaping. The natural query lookup there, {{.Req.URL.Query.Get "hostname"}}, percent-decodes before substituting. Executing that template against a request for /proxy.js?hostname=%22%2balert(document.domain)%2b%22&path=%2Fsimple produces }(window,{},""+alert(document.domain)+"/simple"), giving arbitrary script execution in the origin of whichever domain serves the file.
The same unescaped-substitution shape also applies to {{.Req.Host}} in the non-proxy custom-domain scripts (latest.js, events.js, light.js, v<n>/app.js), where the value comes from the request Host header. Go's net/http host-header validation is permissive enough to admit a quote character, so that path deserves checking too even though it matches the pre-existing nginx behaviour rather than regressing it.
Suggested changes
- Escape every server-substituted value before it reaches the JavaScript string literal, for example by wrapping the Caddy template expressions in Go's built-in js escaper ({{js (.Req.URL.Query.Get "hostname")}}), which preserves percent-decoding while neutralising quotes and backslashes.
- Decide deliberately whether the Caddy setup should keep nginx's raw, still-encoded semantics for hostname and path, or move to decoded values, and record that decision next to the substitution table so future edits do not silently change it.
- Review {{.Req.Host}} substitution in the non-proxy custom-domain scripts against a request carrying a quote in the Host header, and escape it the same way if the server accepts such a header.
- Add a regression test that runs a hostile hostname/path query value through the actual template engine and asserts the resulting script still parses as a single string literal, rather than only asserting the placeholder text.
- Consider having src/default.js read the injected base URL through a form that cannot break out of surrounding syntax, such as a JSON-encoded or separately-declared value, so correctness no longer depends on server-side escaping.
Problem
The proxy variant of the tracking script (dist/latest/custom/proxy.js, built from src/default.js with the {{nginxProxyHost}} placeholder) ends with }(window,{},"<base url>"), where the base URL is filled in by the web server at request time from the hostname and path query parameters. The value lands directly inside a double-quoted JavaScript string literal with no escaping, so any substituted value containing a quote or backslash breaks out of the literal and executes as script on the domain serving the file — a customer-controlled proxy domain.
On the old nginx custom-domains server this is not reachable, because nginx's $arg_hostname and $arg_path return the raw, still percent-encoded query value: a request for ?hostname=%22%2balert(1)%2b%22 emits the literal text %22%2balert(1)%2b%22 and the string literal stays intact.
The Caddy custom-domains setup evaluates the same placeholders through Caddy's templates directive, which uses Go's text/template and applies no contextual escaping. The natural query lookup there, {{.Req.URL.Query.Get "hostname"}}, percent-decodes before substituting. Executing that template against a request for /proxy.js?hostname=%22%2balert(document.domain)%2b%22&path=%2Fsimple produces }(window,{},""+alert(document.domain)+"/simple"), giving arbitrary script execution in the origin of whichever domain serves the file.
The same unescaped-substitution shape also applies to {{.Req.Host}} in the non-proxy custom-domain scripts (latest.js, events.js, light.js, v<n>/app.js), where the value comes from the request Host header. Go's net/http host-header validation is permissive enough to admit a quote character, so that path deserves checking too even though it matches the pre-existing nginx behaviour rather than regressing it.
Suggested changes