Skip to content

capslock terminal output does not escape ANSI control sequences from //line directives #299

@MusGaas

Description

@MusGaas

Description

Filenames carried in capslock's call-path output flow from Go's token.Position.Filename field, which can be set to an arbitrary string by //line <text>:<n> and /*line <text>:<n>*/ directives in any analyzed source file. The Go parser does not validate <text>, so it can contain ESC, CSI, OSC, tabs, or other control bytes.

Three terminal-oriented sinks emit this string without escaping:

  • analyzer/compare.go:162printCallPath for -output=compare
  • cmd/capslock-git-diff/main.go:450printCallPath in capslock-git-diff
  • analyzer/static/verbose.tmpl:10Site.Filename rendered via the verbose template (-output=v / -output=verbose)

Existing JSON output (-output=json) is unaffected because protojson already escapes control bytes.

The maintainers already use strconv.Quote (with surrounding quotes trimmed) for the version string at cmd/capslock/capslock.go:74-77. The same helper applied at the three sinks above closes the gap with the same pattern.

Minimal repro

A single attacker file embedded anywhere in the analyzed module graph (direct or transitive):

// dep/file.go
package dep

import "os"

//line evil:1
func LoadConfig() ([]byte, error) { return os.ReadFile("/etc/hostname") }

Replace evil with any byte sequence including ESC. For example, the bytes \x1b[2J\x1b[H\x1b[32mOK\x1b[0m produce a payload that on a VT100-class terminal clears the screen, homes the cursor, and prints "OK" in green.

Run capslock -packages=./... -output=v against a module that depends on this package.

Expected behavior

The filename portion of the call-path line should be safe to write to any terminal. Control bytes coming from token.Position.Filename should be escaped, in the same way that cmd/capslock/capslock.go:74-77 already escapes the version string.

Actual behavior

The raw bytes from the //line directive are written to stdout. On a VT100-class terminal — which includes most terminal emulators and CI log dashboards (Buildkite, GitLab CI, partial GitHub Actions) — the cursor-control bytes are interpreted, so a crafted directive can overwrite or replace the surrounding capability output. With \t in place of ESC bytes, the same channel reshapes the tabwriter columns even on ANSI-stripping renderers (Jenkins, plain log viewers, log aggregators).

Suggested fix

Apply strconv.Quote(s) with the surrounding "s trimmed to the filename argument at each of the three sinks, mirroring the existing helper at cmd/capslock/capslock.go:74-77. PR follows.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions