Skip to content

Proxy-product hostname and path values are injected unescaped into the served tracking script #96

Description

@github-actions

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions