Skip to content

Commit 3fb40d0

Browse files
authored
fix(redaction): do not redact URI passwords that run through a line break (#3619)
1 parent 4b4f4c9 commit 3fb40d0

5 files changed

Lines changed: 61 additions & 7 deletions

File tree

‎internal/redaction/betterleaks.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ var rulesConfig string
7979
// betterleaks.toml, the betterleaks version, or the handling of findings in
8080
// this file changes what gets redacted, so that policies can tell placeholders
8181
// from different rulesets apart. Version 1 is the plain default ruleset.
82-
const RulesetVersion = 2
82+
const RulesetVersion = 3
8383

8484
var (
8585
defaultScanner = sync.OnceValues(newBetterleaksScanner)

‎internal/redaction/betterleaks.toml‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,19 @@ regexTarget = "match"
5858
# redacted.
5959
regexes = ['''(?i)://(?:localhost|[0-9]{1,3}(?:\.[0-9]{1,3}){3}|[a-z0-9-]+(?:\.[a-z0-9-]+)+):[0-9]{1,5}@''']
6060

61+
[[allowlists]]
62+
description = "A port followed by a line break read as the password of a URI, such as a port followed by more lines up to an email address"
63+
targetRules = ["generic-credential-uri"]
64+
# The password stops at whitespace in source text, but in the JSON-encoded text
65+
# a line break is the escape \n, so the match runs on into later lines up to
66+
# the next @. Only a password that is a port, and then a line break, is kept: a
67+
# real password followed by a line break is still redacted. A real URI on a
68+
# later line has its own ://, which the password cannot run through, so it is
69+
# still found. Only an escape counts: an odd run of backslashes before the n, r
70+
# or t. A \n written in source code reaches the scanner as \\n and is not a
71+
# line break.
72+
regexes = ['''^[0-9]{1,5}(?:\\\\)*\\[nrt]''']
73+
6174
[[allowlists]]
6275
description = "An elided password in documentation, such as http://user:…@example.com"
6376
targetRules = ["generic-credential-uri"]

‎internal/redaction/precision_test.go‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ func TestRulesConfigKeepsNonSecrets(t *testing.T) {
139139
text: "issuer: http://localhost:8000@chainloop.local\n",
140140
wantDefaultRule: ruleCredentialURI,
141141
},
142+
{
143+
// The file content of a Write tool call. The scanner sees it
144+
// JSON-encoded, so the password of the match ran from the port,
145+
// through the escaped line breaks, to the user part of an email
146+
// address on a later line.
147+
name: "uri password running through line breaks to an email address",
148+
text: "API_URL=http://localhost:48002\nCAS_ENDPOINT=localhost:49001\nTEST_EMAIL=" + sampleEmail + "\n",
149+
wantDefaultRule: ruleCredentialURI,
150+
},
142151
}
143152

144153
for _, tc := range testCases {
@@ -213,6 +222,19 @@ func TestRulesConfigStillRedactsSecrets(t *testing.T) {
213222
secret: samplePassword,
214223
wantRule: ruleCredentialURI,
215224
},
225+
{
226+
name: "password in a connection uri on the line after a host and port",
227+
text: "API_URL=http://localhost:48002\nDSN=postgres://admin:" + samplePassword + "@db.internal:5432/app\n",
228+
secret: samplePassword,
229+
wantRule: ruleCredentialURI,
230+
keep: "localhost:48002",
231+
},
232+
{
233+
name: "password in a uri followed by a line break and an email address",
234+
text: "API_URL=http://admin:" + samplePassword + "\nTEST_EMAIL=" + sampleEmail + "\n",
235+
secret: samplePassword,
236+
wantRule: ruleCredentialURI,
237+
},
216238
}
217239

218240
for _, tc := range testCases {

‎pkg/attestation/crafter/materials/aicodingsession/redact_test.go‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ const (
5050
fixtureAnthropicKey = "sk-ant-api03-sT5wsx9DwmaHZDL0dUWKNhAhULxa35sUzyLFK9" +
5151
"5QBTZMDJTYn8p0J7ZQbwpYGYCQeW5eXAAGtVSmhp7UO9vxHJtSBC0xpAA"
5252
fixtureRepository = "https://oauth2:" + fixtureGitHubPAT + "@github.com/example/repo.git"
53+
54+
// The digests of testdata/session-fp-shaped.json are not secrets, but a
55+
// high-entropy hex string in a file is flagged by the repository scan.
56+
fixtureDigestA = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c6f9e8d7c6b5a4938271605f4e"
57+
fixtureDigestB = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c69f8e7d6c5b4a39281706f5e4"
58+
// The UUID and commit ids are values the generic rules must leave alone.
59+
fixtureUUID = "3bf79921-3c03-81b6-" + "afff-cb246849866f"
60+
fixtureCommitStart = "9f8e7d6c5b4a39281706f5e4d3c2b1a0" + "f9e8d7c6"
61+
fixtureCommitEnd = "e25fa85e1c4b7a9d3f082165" + "0cd7e4b39a1f8c2d"
5362
)
5463

5564
// fixtureSecrets maps each placeholder in the session fixtures to the value the
@@ -61,6 +70,11 @@ var fixtureSecrets = map[string]string{
6170
"__GITHUB_PAT__": fixtureGitHubPAT,
6271
"__ANTHROPIC_API_KEY__": fixtureAnthropicKey,
6372
"__GIT_REPOSITORY_WITH_CREDENTIALS__": fixtureRepository,
73+
"__SHA256_DIGEST_A__": fixtureDigestA,
74+
"__SHA256_DIGEST_B__": fixtureDigestB,
75+
"__SESSION_UUID__": fixtureUUID,
76+
"__COMMIT_START__": fixtureCommitStart,
77+
"__COMMIT_END__": fixtureCommitEnd,
6478
}
6579

6680
// readFixture loads a session fixture with its credential placeholders resolved.

‎pkg/attestation/crafter/materials/aicodingsession/testdata/session-fp-shaped.json‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"version": "2.1.83"
99
},
1010
"session": {
11-
"id": "3bf79921-3c03-81b6-afff-cb246849866f",
11+
"id": "__SESSION_UUID__",
1212
"slug": "memoized-purring-valley",
1313
"started_at": "2026-03-25T15:10:49.161Z",
1414
"ended_at": "2026-03-25T16:59:14.988Z",
@@ -18,8 +18,8 @@
1818
"repository": "git@github.com:chainloop-dev/chainloop.git",
1919
"branch": "jiparis/pfm-6984-redact-secrets-in-session",
2020
"work_dir": "/Users/jiparis/projects/chainloop",
21-
"commit_start": "9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6",
22-
"commit_end": "e25fa85e1c4b7a9d3f0821650cd7e4b39a1f8c2d",
21+
"commit_start": "__COMMIT_START__",
22+
"commit_end": "__COMMIT_END__",
2323
"commits": [
2424
"9f8e7d6 perf(crafter): de-duplicate AccessChk security descriptors",
2525
"e25fa85 Bump Helm Chart and Dagger Version"
@@ -32,7 +32,7 @@
3232
"lines_removed": 87,
3333
"files": [
3434
{"path": "app/controlplane/internal/service/attestation.go", "status": "modified", "attribution": "ai"},
35-
{"path": "pkg/attestation/crafter/materials/aicodingsession/redact.go", "status": "created", "attribution": "ai", "session_ids": ["3bf79921-3c03-81b6-afff-cb246849866f"]}
35+
{"path": "pkg/attestation/crafter/materials/aicodingsession/redact.go", "status": "created", "attribution": "ai", "session_ids": ["__SESSION_UUID__"]}
3636
]
3737
},
3838
"model": {
@@ -72,7 +72,7 @@
7272
"main": [
7373
{
7474
"type": "user",
75-
"message": {"role": "user", "content": "the sha256 digest is 9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c6f9e8d7c6b5a4938271605f4e"},
75+
"message": {"role": "user", "content": "the sha256 digest is __SHA256_DIGEST_A__"},
7676
"timestamp": "2026-03-25T15:13:37.330Z"
7777
},
7878
{
@@ -82,7 +82,7 @@
8282
},
8383
{
8484
"type": "user",
85-
"message": {"role": "user", "content": "Pushed the attestation.\n digest: sha256:9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c69f8e7d6c5b4a39281706f5e4\n access sha256:9f8e7d6c5b4a39281706f5e4d3c2b1a0f9e8d7c69f8e7d6c5b4a39281706f5e4\nworkflow key: 3bf79921-3c03-81b6-afff-cb246849866f\nidempotency_key=3bf79921-3c03-81b6-afff-cb246849866f\n"},
85+
"message": {"role": "user", "content": "Pushed the attestation.\n digest: sha256:__SHA256_DIGEST_B__\n access sha256:__SHA256_DIGEST_B__\nworkflow key: __SESSION_UUID__\nidempotency_key=__SESSION_UUID__\n"},
8686
"timestamp": "2026-03-25T15:14:02.000Z"
8787
},
8888
{
@@ -109,6 +109,11 @@
109109
"type": "user",
110110
"message": {"role": "user", "content": "119\tconst pat = \"__GITHUB_PAT__\" // gitleaks:allow\n120\twantURI := \"https://tracker.example.com/issue/1?token=\" + pat\n"},
111111
"timestamp": "2026-03-25T15:14:07.000Z"
112+
},
113+
{
114+
"type": "assistant",
115+
"message": {"role": "assistant", "content": [{"type": "tool_use", "name": "Write", "input": {"file_path": "/work/e2e/.env", "content": "ENVIRONMENT=local\nBASE_URL=http://localhost:43000\nAPI_URL=http://localhost:48002\nCAS_ENDPOINT=localhost:49001\nCHAINLOOP_API_INSECURE=true\nTEST_EMAIL=john@chainloop.local\n"}}]},
116+
"timestamp": "2026-03-25T15:14:08.000Z"
112117
}
113118
]
114119
},

0 commit comments

Comments
 (0)