detached 7121811

fix: escape all ASCII control chars in CLF/Combined access log fields

commit
71218113a3b507b8703f27a9e6fddab59081f6a5
author
brice · · 2026-04-03 15:52 UTC
parent
01b6bc5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -8
M internal/adapters/out/accesslog/formatter.go +25 -8
@@ -110,13 +110,30 @@ func buildRequestTarget(path, query string) string {
110110 return path + "?" + query
111111 }
112112
113-// escapeQuoted escapes backslashes, double-quotes, and newline characters inside
114-// a value that will be enclosed in double-quotes in the log line. Newline
115-// escaping prevents log-injection via user-controlled fields such as User-Agent.
113+// escapeQuoted escapes backslashes, double-quotes, and control characters inside
114+// a value that will be enclosed in double-quotes in the log line.
115+// All ASCII control characters (0x00-0x1F) are escaped to prevent log-injection
116+// via user-controlled fields such as User-Agent or Referer.
116117 func escapeQuoted(s string) string {
117- s = strings.ReplaceAll(s, `\`, `\\`)
118- s = strings.ReplaceAll(s, `"`, `\"`)
119- s = strings.ReplaceAll(s, "\n", `\n`)
120- s = strings.ReplaceAll(s, "\r", `\r`)
121- return s
118+ var b strings.Builder
119+ b.Grow(len(s))
120+ for _, r := range s {
121+ switch {
122+ case r == '\\':
123+ b.WriteString(`\\`)
124+ case r == '"':
125+ b.WriteString(`\"`)
126+ case r == '\n':
127+ b.WriteString(`\n`)
128+ case r == '\r':
129+ b.WriteString(`\r`)
130+ case r == '\t':
131+ b.WriteString(`\t`)
132+ case r < 0x20:
133+ fmt.Fprintf(&b, `\x%02x`, r)
134+ default:
135+ b.WriteRune(r)
136+ }
137+ }
138+ return b.String()
122139 }
@@ -110,13 +110,30 @@ func buildRequestTarget(path, query string) string {
110 return path + "?" + query110 return path + "?" + query
111 }111 }
112 112
113-// escapeQuoted escapes backslashes, double-quotes, and newline characters inside113+// escapeQuoted escapes backslashes, double-quotes, and control characters inside
114-// a value that will be enclosed in double-quotes in the log line. Newline114+// a value that will be enclosed in double-quotes in the log line.
115-// escaping prevents log-injection via user-controlled fields such as User-Agent.115+// All ASCII control characters (0x00-0x1F) are escaped to prevent log-injection
116+// via user-controlled fields such as User-Agent or Referer.
116 func escapeQuoted(s string) string {117 func escapeQuoted(s string) string {
117- s = strings.ReplaceAll(s, `\`, `\\`)118+ var b strings.Builder
118- s = strings.ReplaceAll(s, `"`, `\"`)119+ b.Grow(len(s))
119- s = strings.ReplaceAll(s, "\n", `\n`)120+ for _, r := range s {
120- s = strings.ReplaceAll(s, "\r", `\r`)121+ switch {
121- return s122+ case r == '\\':
123+ b.WriteString(`\\`)
124+ case r == '"':
125+ b.WriteString(`\"`)
126+ case r == '\n':
127+ b.WriteString(`\n`)
128+ case r == '\r':
129+ b.WriteString(`\r`)
130+ case r == '\t':
131+ b.WriteString(`\t`)
132+ case r < 0x20:
133+ fmt.Fprintf(&b, `\x%02x`, r)
134+ default:
135+ b.WriteRune(r)
136+ }
137+ }
138+ return b.String()
122 }139 }
M internal/adapters/out/accesslog/writer.go +2 -0
@@ -160,6 +160,8 @@ func newFileSink(cfg Config) (*fileSink, error) {
160160 return nil, fmt.Errorf("accesslog: create log directory: %w", err)
161161 }
162162
163+ // lumberjack defaults when zero: MaxSize=100MB, MaxBackups=keep all, MaxAge=no limit.
164+ // Config defaults in run.go set these to sensible values; zero here is valid.
163165 return &fileSink{
164166 logger: &lumberjack.Logger{
165167 Filename: cfg.FilePath,
@@ -160,6 +160,8 @@ func newFileSink(cfg Config) (*fileSink, error) {
160 return nil, fmt.Errorf("accesslog: create log directory: %w", err)160 return nil, fmt.Errorf("accesslog: create log directory: %w", err)
161 }161 }
162 162
163+ // lumberjack defaults when zero: MaxSize=100MB, MaxBackups=keep all, MaxAge=no limit.
164+ // Config defaults in run.go set these to sensible values; zero here is valid.
163 return &fileSink{165 return &fileSink{
164 logger: &lumberjack.Logger{166 logger: &lumberjack.Logger{
165 Filename: cfg.FilePath,167 Filename: cfg.FilePath,

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close