Skip to content

internal/graph: escape file and title strings written to dot - #1053

Open
Medios-lgtm wants to merge 3 commits into
google:mainfrom
Medios-lgtm:graph-escape-file-title-dot
Open

Medios-lgtm wants to merge 3 commits into
google:mainfrom
Medios-lgtm:graph-escape-file-title-dot

Conversation

@Medios-lgtm

@Medios-lgtm Medios-lgtm commented Oct 20, 2026 •

Copy link
Copy Markdown

Mapping/object file names and the graph title come from the profile, but three sites in internal/graph/dotgraph.go wrote them into the generated dot without escaping: the digraph name in start(), the legend tooltip in addLegend(), and the File/Objfile components of a node label in multilinePrintableName(). A " in any of them closes the quoted attribute, so a crafted profile can inject dot attributes such as URL=, which Graphviz turns into an xlink:href in the SVG the web UI renders inline. The sibling node and edge tooltips already run through escapeForDot; this wraps the three missed sites the same way.

Repro: a profile whose mapping file name is lib" URL="javascript:alert(1) renders, before this change, an <a xlink:href="javascript:alert(1)"> in pprof -svg / -http; after it the quote is escaped and no such link appears. TestComposeWithNamesThatNeedEscaping now checks the escaping directly instead of against compose7.dot (removed); it fails on the three unescaped sites and passes with the fix.

Mapping/object file names and the graph title come from the profile and
are attacker-controlled, but three sites wrote them into the dot output
without escaping: the digraph name in start(), the legend tooltip in
addLegend(), and the File/Objfile components of a node label in
multilinePrintableName(). A double quote in any of them closes the
quoted attribute and lets the profile inject further dot attributes
(for example a URL=), which Graphviz emits as an xlink:href in the SVG
that the web UI renders inline. Wrap each site in escapeForDot, matching
the sibling node and edge tooltips that already escape.
aalexand
aalexand previously approved these changes Oct 21, 2026
@aalexand
aalexand enabled auto-merge (squash) October 21, 2026 22:56
@aalexand
aalexand disabled auto-merge October 21, 2026 22:57
@codecov-commenter

codecov-commenter commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.45%. Comparing base (1facf8f) to head (d49732b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1053      +/-   ##
==========================================
+ Coverage   73.42%   73.45%   +0.03%     
==========================================
  Files          46       46              
  Lines        7936     7938       +2     
==========================================
+ Hits         5827     5831       +4     
+ Misses       2086     2084       -2     
  Partials       23       23              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread internal/graph/dotgraph_test.go Outdated
compareGraphs(t, buf.Bytes(), "compose7.dot")
}

func TestComposeEscapesFileAndTitleInDot(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we instead update the existing test like in https://github.com/google/pprof/pull/1032/changes?

One thing I like about this test here though is that it doesn't depend on the file stored elsewhere and encodes the checks directly. But maybe we can update that existing test to do this for all escaping checks universally and remove the testdata *.dot file?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the checks into TestComposeWithNamesThatNeedEscaping and dropped compose7.dot. It now covers the function name, file, objfile, title and legend label, and asserts directly that each one shows up escaped and never raw, so the existing name cases are still checked without the golden file. Still fails on the three unescaped sites without the fix.

…atNeedEscaping

Fold the file, objfile and title cases into the existing escaping test
and have it assert on the escaped and unescaped forms of each string
directly, so the compose7.dot golden is no longer needed.

@aalexand aalexand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this.

Comment thread internal/graph/dotgraph_test.go Outdated
for _, raw := range []string{`evil.go" peripheries=3`, `evil.so" peripheries=3`, `prof" bgcolor="red`} {
if strings.Contains(out, raw) {
t.Errorf("unescaped %q in dot output allows attribute injection:\n%s", raw, out)
for _, s := range []string{`var"src"`, `file"name.go`, `obj"file.so`, `test"title`, `label3: "foo"`} {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

label3 does not have a character to escape I think? Or am I missing something?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It did, but only indirectly: baseAttrsAndConfig defines that label as label3: "foo", so the quote was hiding in the helper and the test was leaning on it to cover the legend labels that compose7.dot used to check. I made it explicit by setting c.Labels in the test to label"text so the character being escaped is visible right there.

Comment thread internal/graph/dotgraph_test.go Outdated
for _, escaped := range []string{`evil.go\" peripheries=3`, `evil.so\" peripheries=3`, `prof\" bgcolor=\"red`} {
if !strings.Contains(out, escaped) {
t.Errorf("expected escaped %q in dot output:\n%s", escaped, out)
if want := escapeForDot(s); !strings.Contains(got, want) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe include the expected escaped strings also directly in the test, without calling this same function that the code is calling? I think that way it would be more visual / explicit.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Switched it to a table of raw/escaped pairs with the expected strings written out literally (e.g. var"src" -> var\"src\"), so the test no longer calls escapeForDot itself. Still fails on the file, objfile and title sites without the fix.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants