Redact quoted credentials in Git diagnostics
This commit is contained in:
@@ -4,14 +4,20 @@ Reviewed `2f95753`, including the implementation commits after `a1dc705`,
|
|||||||
against the accepted [audit target](audit.md) and completed
|
against the accepted [audit target](audit.md) and completed
|
||||||
[implementation plan](implementation.md). A follow-up integration audit reviewed
|
[implementation plan](implementation.md). A follow-up integration audit reviewed
|
||||||
`c8fa165`, whose production code is identical, and added R02 and R03. Findings
|
`c8fa165`, whose production code is identical, and added R02 and R03. Findings
|
||||||
below identify remaining work; production code and existing tests were unchanged
|
and completion records below track the remediation. Production code and existing
|
||||||
during both reviews.
|
tests were unchanged during both reviews.
|
||||||
|
|
||||||
## R01. Git diagnostic redaction misses apostrophes in credentials
|
## R01. Git diagnostic redaction misses apostrophes in credentials
|
||||||
|
|
||||||
**Priority:** P2. **Status:** Pending. This is a remaining gap in A04 and
|
**Priority:** P2. **Status:** Complete. This addressed a gap in A04 and
|
||||||
[Stage 6](implementation.md#stage-6-sanitize-git-endpoint-diagnostics).
|
[Stage 6](implementation.md#stage-6-sanitize-git-endpoint-diagnostics).
|
||||||
|
|
||||||
|
**Completion:** Diagnostic matching recognizes complete userinfo before
|
||||||
|
surrounding quotation, preserving error context and wrapped causes. Native Git,
|
||||||
|
literal/encoded apostrophe, malformed userinfo, and wrapped-error regressions
|
||||||
|
pass; the new regressions failed before the fix. The complete `internal/git`
|
||||||
|
suite and public/CLI endpoint-mismatch redaction tests passed offline.
|
||||||
|
|
||||||
**Relevant code:** `httpURLSpan` and `displayDiagnostic` in
|
**Relevant code:** `httpURLSpan` and `displayDiagnostic` in
|
||||||
[internal/git/display.go](../../internal/git/display.go), lines 9 and 32–39;
|
[internal/git/display.go](../../internal/git/display.go), lines 9 and 32–39;
|
||||||
`pushURLs` and `run` in
|
`pushURLs` and `run` in
|
||||||
@@ -143,10 +149,10 @@ sequences; build settings have one internal representation; artifact declaration
|
|||||||
matching is shared; neutral delivery policy remains separate from Gitea protocol
|
matching is shared; neutral delivery policy remains separate from Gitea protocol
|
||||||
operations; and private CLI handlers own presentation.
|
operations; and private CLI handlers own presentation.
|
||||||
|
|
||||||
R01–R03 are the remaining findings from the initial review and follow-up
|
R01–R03 record findings and remediation status from the initial review and
|
||||||
integration audit. Package ownership and public facade shape match the target;
|
follow-up integration audit. Package ownership and public facade shape match
|
||||||
the findings concern diagnostic and subprocess-environment behavior across
|
the target; the findings concern diagnostic and subprocess-environment behavior
|
||||||
existing boundaries. The review identified no inadvertently strengthened public
|
across existing boundaries. The review identified no inadvertently strengthened public
|
||||||
contract. The selected redirect and diagnostic limits match the accepted scope.
|
contract. The selected redirect and diagnostic limits match the accepted scope.
|
||||||
JSON response caps and production exclusion indexing remain explicitly deferred
|
JSON response caps and production exclusion indexing remain explicitly deferred
|
||||||
with assessment evidence, as planned.
|
with assessment evidence, as planned.
|
||||||
@@ -187,7 +193,8 @@ inherited-pipe cancellation, forwarding errors, joined cleanup outcomes,
|
|||||||
bounded asset reads, upload reconciliation, and tree-reader cleanup precedence.
|
bounded asset reads, upload reconciliation, and tree-reader cleanup precedence.
|
||||||
|
|
||||||
R02 and R03 were reproduced independently with temporary Go test overlays.
|
R02 and R03 were reproduced independently with temporary Go test overlays.
|
||||||
Their assertions failed against the current implementation; R02's comparison
|
Their assertions failed against the reviewed implementation; R02's comparison
|
||||||
using the earlier Git environment behavior passed. The checkout contains no
|
using the earlier Git environment behavior passed. Both audits left production
|
||||||
probe code or production changes. The previously passing full-suite, static,
|
code and existing tests unchanged. The full-suite, static, and compiled
|
||||||
and compiled API/dependency checks apply to the unchanged production sources.
|
API/dependency results above record the audit baseline; completion notes record
|
||||||
|
verification of the subsequent repairs.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpURLSpan = regexp.MustCompile(`(?i)https?://[^\s"'<>]+`)
|
// Match userinfo through @ before treating quotes as diagnostic delimiters.
|
||||||
|
var httpURLSpan = regexp.MustCompile(`(?i)https?://(?:[^\s/?#<>]*@)?[^\s"'<>]*`)
|
||||||
|
|
||||||
// DisplayURL hides HTTP userinfo without changing the endpoint used by Git.
|
// DisplayURL hides HTTP userinfo without changing the endpoint used by Git.
|
||||||
func DisplayURL(raw string) string {
|
func DisplayURL(raw string) string {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func TestDisplayURL(t *testing.T) {
|
|||||||
for _, test := range []struct{ input, want string }{
|
for _, test := range []struct{ input, want string }{
|
||||||
{"https://user:password@example.test/repo", "https://redacted@example.test/repo"},
|
{"https://user:password@example.test/repo", "https://redacted@example.test/repo"},
|
||||||
{"https://token@example.test/repo", "https://redacted@example.test/repo"},
|
{"https://token@example.test/repo", "https://redacted@example.test/repo"},
|
||||||
|
{"https://user:secret'part@example.test/repo", "https://redacted@example.test/repo"},
|
||||||
{"https://user%40example:secret%2Fvalue@example.test/repo", "https://redacted@example.test/repo"},
|
{"https://user%40example:secret%2Fvalue@example.test/repo", "https://redacted@example.test/repo"},
|
||||||
{"http://broken%zz:secret@example.test/repo", "[redacted URL]"},
|
{"http://broken%zz:secret@example.test/repo", "[redacted URL]"},
|
||||||
{"https://example.test/repo", "https://example.test/repo"},
|
{"https://example.test/repo", "https://example.test/repo"},
|
||||||
@@ -23,6 +24,32 @@ func TestDisplayURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDisplayDiagnosticRedactsUserinfoBeforeQuotedContext(t *testing.T) {
|
||||||
|
for _, test := range []struct{ input, want string }{
|
||||||
|
{"fatal: 'https://user:secret'part@example.test/repo': denied", "fatal: 'https://redacted@example.test/repo': denied"},
|
||||||
|
{"failed at https://token'part@example.test", "failed at https://redacted@example.test"},
|
||||||
|
{"fatal: 'https://user:secret%27part@example.test/repo': denied", "fatal: 'https://redacted@example.test/repo': denied"},
|
||||||
|
{"fatal: 'https://broken%zz:secret'part@example.test/repo': denied", "fatal: '[redacted URL]': denied"},
|
||||||
|
{"fatal: 'https://token'part@': denied", "fatal: '[redacted URL]': denied"},
|
||||||
|
{"see 'https://example.test/path' and git@example.test:repo", "see 'https://example.test/path' and git@example.test:repo"},
|
||||||
|
} {
|
||||||
|
if got := displayDiagnostic(test.input); got != test.want {
|
||||||
|
t.Errorf("displayDiagnostic(%q) = %q, want %q", test.input, got, test.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveRedactsCredentialsInNativeGitErrors(t *testing.T) {
|
||||||
|
fixture := newRemoteFixture(t)
|
||||||
|
for _, remote := range []string{"https://user:secret'part@example.test/repo", "https://user:secret%27part@example.test/repo"} {
|
||||||
|
_, err := Resolve(t.Context(), fixture.work, remote)
|
||||||
|
var exit *exec.ExitError
|
||||||
|
if !errors.As(err, &exit) || strings.Contains(err.Error(), "secret") || strings.Contains(err.Error(), "user:") || !strings.Contains(err.Error(), "example.test/repo") {
|
||||||
|
t.Fatalf("unsafe or incomplete native Git diagnostic: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGitFailuresRedactCredentialsAndPreserveCause(t *testing.T) {
|
func TestGitFailuresRedactCredentialsAndPreserveCause(t *testing.T) {
|
||||||
installFakeGit(t, `case "$*" in
|
installFakeGit(t, `case "$*" in
|
||||||
*check-ref-format*) exit 0 ;;
|
*check-ref-format*) exit 0 ;;
|
||||||
@@ -53,10 +80,12 @@ exit 23`)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDisplayErrorPreservesWrappedCause(t *testing.T) {
|
func TestDisplayErrorPreservesWrappedCause(t *testing.T) {
|
||||||
cause := errors.New("failed at https://token@example.test/path")
|
for _, userinfo := range []string{"token", "token'part", "token%27part"} {
|
||||||
err := displayError{cause: cause}
|
cause := errors.New("failed at https://" + userinfo + "@example.test/path")
|
||||||
if !errors.Is(err, cause) || strings.Contains(err.Error(), "token") {
|
err := displayError{cause: cause}
|
||||||
t.Fatalf("sanitized error = %v", err)
|
if !errors.Is(err, cause) || strings.Contains(err.Error(), "token") {
|
||||||
|
t.Fatalf("sanitized error = %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user