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:162 — printCallPath for -output=compare
cmd/capslock-git-diff/main.go:450 — printCallPath in capslock-git-diff
analyzer/static/verbose.tmpl:10 — Site.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.
Description
Filenames carried in capslock's call-path output flow from Go's
token.Position.Filenamefield, 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:162—printCallPathfor-output=comparecmd/capslock-git-diff/main.go:450—printCallPathin capslock-git-diffanalyzer/static/verbose.tmpl:10—Site.Filenamerendered via the verbose template (-output=v/-output=verbose)Existing JSON output (
-output=json) is unaffected becauseprotojsonalready escapes control bytes.The maintainers already use
strconv.Quote(with surrounding quotes trimmed) for the version string atcmd/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):
Replace
evilwith any byte sequence including ESC. For example, the bytes\x1b[2J\x1b[H\x1b[32mOK\x1b[0mproduce a payload that on a VT100-class terminal clears the screen, homes the cursor, and prints "OK" in green.Run
capslock -packages=./... -output=vagainst 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.Filenameshould be escaped, in the same way thatcmd/capslock/capslock.go:74-77already escapes the version string.Actual behavior
The raw bytes from the
//linedirective 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\tin 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 atcmd/capslock/capslock.go:74-77. PR follows.