Skip to content

Commit 7ce7828

Browse files
dreynaud-oaittaylorr-oai
authored andcommitted
trace2: redact URLs in error events
Transport errors can include a signed URL in the message passed to error() or die(). Argument redaction does not cover these messages, so a failed request can still record its password or signature in Trace2. Have each target redact its formatted error payload. Keep the existing va_list callback contract, including each target's use of va_copy(), so the caller can still use the arguments when writing to stderr. Redact the JSON target's separate format field as well, accounting for doubled percent signs in literal query parameter names. Use whitespace, double quotes, and angle brackets to delimit URLs in error messages. Keep a trailing quote for a single-quoted token, but treat other punctuation as part of the URL: an apostrophe inside a signature is not a reliable delimiter. Continue scanning for adjacent or nested URLs after redacting each match. Signed-off-by: Daniel Reynaud <dreynaud@openai.com> Signed-off-by: Taylor Blau <ttaylorr@openai.com>
1 parent 79fa41c commit 7ce7828

6 files changed

Lines changed: 126 additions & 8 deletions

File tree

t/t0212-trace2-event.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,36 @@ test_expect_success 'signature query parameters are redacted' '
375375
done
376376
'
377377

378+
test_expect_success 'error targets redact URLs without changing stderr' '
379+
test_when_finished "rm trace.normal trace.perf trace.event stderr" &&
380+
url="https://example.com/?sig=secret." &&
381+
message="unable to access $SQ$url$SQ" &&
382+
expect="unable to access ${SQ}https://example.com/?sig=<REDACTED>$SQ" &&
383+
GIT_TRACE2="$(pwd)/trace.normal" \
384+
GIT_TRACE2_PERF="$(pwd)/trace.perf" \
385+
GIT_TRACE2_EVENT="$(pwd)/trace.event" \
386+
test-tool trace2 003error "$message" 2>stderr &&
387+
test_grep -F "$expect" trace.normal &&
388+
test_grep -F "$expect" trace.perf &&
389+
test_grep -F "$expect" trace.event &&
390+
test_grep -F "\"fmt\":\"%s\"" trace.event &&
391+
test_grep -F "$message" stderr
392+
'
393+
394+
test_expect_success 'error redaction finds adjacent URLs' '
395+
test_when_finished "rm trace.event stderr" &&
396+
message="https://example.com/#fragment,https://example.com/?sig=secret" &&
397+
GIT_TRACE2_EVENT="$(pwd)/trace.event" \
398+
test-tool trace2 003error "$message" 2>stderr &&
399+
test_grep "\"event\":\"error\".*sig=<REDACTED>" trace.event
400+
'
401+
402+
test_expect_success 'URL signature redaction can be disabled' '
403+
test_when_finished "rm trace.event stderr" &&
404+
GIT_TRACE2_REDACT=0 GIT_TRACE2_EVENT="$(pwd)/trace.event" \
405+
test-tool trace2 003error "https://example.com/?sig=secret" 2>stderr &&
406+
test_grep "\"event\":\"start\".*sig=secret" trace.event &&
407+
test_grep "\"event\":\"error\".*sig=secret" trace.event
408+
'
409+
378410
test_done

trace2.c

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -250,18 +250,30 @@ int trace2_is_enabled(void)
250250
return trace2_enabled;
251251
}
252252

253-
static int is_signature_parameter(const char *parameter)
253+
static int is_signature_parameter(const char *parameter, int is_format)
254254
{
255-
char *name = url_decode_parameter_name(&parameter);
256-
char *p;
255+
struct strbuf unescaped = STRBUF_INIT;
256+
char *name, *p;
257257
int ret;
258258

259+
/* A literal percent sign is doubled in a printf format string. */
260+
if (is_format) {
261+
while (*parameter && *parameter != '=') {
262+
if (parameter[0] == '%' && parameter[1] == '%')
263+
parameter++;
264+
strbuf_addch(&unescaped, *parameter++);
265+
}
266+
parameter = unescaped.buf;
267+
}
268+
269+
name = url_decode_parameter_name(&parameter);
259270
for (p = name; *p; p++)
260271
*p = tolower(*p);
261272
ret = !strcmp(name, "sig") || !strcmp(name, "signature") ||
262273
ends_with(name, "-signature");
263274

264275
free(name);
276+
strbuf_release(&unescaped);
265277
return ret;
266278
}
267279

@@ -271,7 +283,7 @@ static int is_signature_parameter(const char *parameter)
271283
* Returns the original if nothing needed to be redacted.
272284
* Returns a pointer that needs to be `free()`d otherwise.
273285
*/
274-
static const char *redact_arg(const char *arg)
286+
static const char *redact_url(const char *arg, int is_format)
275287
{
276288
const char *p, *colon;
277289
const char *unredacted = arg;
@@ -303,7 +315,7 @@ static const char *redact_arg(const char *arg)
303315
const char *equals = memchr(p, '=', end - p);
304316

305317
if (equals && equals + 1 < end &&
306-
is_signature_parameter(p)) {
318+
is_signature_parameter(p, is_format)) {
307319
strbuf_add(&buf, unredacted,
308320
equals + 1 - unredacted);
309321
strbuf_addstr(&buf, "<REDACTED>");
@@ -319,6 +331,55 @@ static const char *redact_arg(const char *arg)
319331
return strbuf_detach(&buf, NULL);
320332
}
321333

334+
static const char *redact_arg(const char *arg)
335+
{
336+
return redact_url(arg, 0);
337+
}
338+
339+
void tr2_redact_error(struct strbuf *buf, int is_format)
340+
{
341+
size_t pos = 0;
342+
343+
if (!trace2_redact)
344+
return;
345+
346+
while (pos < buf->len) {
347+
const char *p = strcasestr(buf->buf + pos, "http");
348+
const char *end, *scheme_end;
349+
const char *redacted;
350+
char *url;
351+
size_t start;
352+
353+
if (!p)
354+
break;
355+
start = p - buf->buf;
356+
pos = start + 4;
357+
if (!skip_iprefix(p, "http://", &scheme_end) &&
358+
!skip_iprefix(p, "https://", &scheme_end))
359+
continue;
360+
361+
/*
362+
* Whitespace, double quotes and angle brackets delimit URLs.
363+
* Other punctuation can belong to a signature, so preserve a
364+
* single quote only at the end of a single-quoted token.
365+
*/
366+
end = p + strcspn(p, " \t\r\n\v\f\"<>");
367+
if (start && p[-1] == '\'' && end[-1] == '\'')
368+
end--;
369+
370+
/* Also examine any further URLs in the unredacted text. */
371+
pos = scheme_end - buf->buf;
372+
url = xmemdupz(p, end - p);
373+
redacted = redact_url(url, is_format);
374+
if (redacted != url) {
375+
strbuf_splice(buf, start, end - p,
376+
redacted, strlen(redacted));
377+
free((char *)redacted);
378+
}
379+
free(url);
380+
}
381+
}
382+
322383
/*
323384
* Redacts arguments in an argument list.
324385
*

trace2/tr2_tgt.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
struct child_process;
55
struct repository;
6+
struct strbuf;
67
struct json_writer;
78
struct tr2_timer_metadata;
89
struct tr2_timer;
@@ -157,4 +158,10 @@ extern struct tr2_tgt tr2_tgt_event;
157158
extern struct tr2_tgt tr2_tgt_normal;
158159
extern struct tr2_tgt tr2_tgt_perf;
159160

161+
/*
162+
* Redact URLs in an error payload before writing it. Set is_format when
163+
* redacting a printf format string, where literal '%' characters are doubled.
164+
*/
165+
void tr2_redact_error(struct strbuf *buf, int is_format);
166+
160167
#endif /* TR2_TGT_H */

trace2/tr2_tgt_event.c

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,15 +238,31 @@ static void fn_error_va_fl(const char *file, int line, const char *fmt,
238238

239239
jw_object_begin(&jw, 0);
240240
event_fmt_prepare(event_name, file, line, NULL, &jw);
241-
maybe_add_string_va(&jw, "msg", fmt, ap);
241+
if (fmt && *fmt) {
242+
struct strbuf message = STRBUF_INIT;
243+
va_list copy_ap;
244+
245+
va_copy(copy_ap, ap);
246+
strbuf_vaddf(&message, fmt, copy_ap);
247+
va_end(copy_ap);
248+
tr2_redact_error(&message, 0);
249+
jw_object_string(&jw, "msg", message.buf);
250+
strbuf_release(&message);
251+
}
242252
/*
243253
* Also emit the format string as a field in case
244254
* post-processors want to aggregate common error
245255
* messages by type without argument fields (such
246256
* as pathnames or branch names) cluttering it up.
247257
*/
248-
if (fmt && *fmt)
249-
jw_object_string(&jw, "fmt", fmt);
258+
if (fmt && *fmt) {
259+
struct strbuf format = STRBUF_INIT;
260+
261+
strbuf_addstr(&format, fmt);
262+
tr2_redact_error(&format, 1);
263+
jw_object_string(&jw, "fmt", format.buf);
264+
strbuf_release(&format);
265+
}
250266
jw_end(&jw);
251267

252268
tr2_dst_write_line(&tr2dst_event, &jw.json);

trace2/tr2_tgt_normal.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ static void fn_error_va_fl(const char *file, int line, const char *fmt,
152152
strbuf_addch(&buf_payload, ' ');
153153
maybe_append_string_va(&buf_payload, fmt, ap);
154154
}
155+
tr2_redact_error(&buf_payload, 0);
155156
normal_io_write_fl(file, line, &buf_payload);
156157
strbuf_release(&buf_payload);
157158
}

trace2/tr2_tgt_perf.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ static void fn_error_va_fl(const char *file, int line, const char *fmt,
241241
struct strbuf buf_payload = STRBUF_INIT;
242242

243243
maybe_append_string_va(&buf_payload, fmt, ap);
244+
tr2_redact_error(&buf_payload, 0);
244245

245246
perf_io_write_fl(file, line, event_name, NULL, NULL, NULL, NULL,
246247
&buf_payload);

0 commit comments

Comments
 (0)