Skip to content

[BUG] Credentials embedded in configured URLs leak into logs, error messages and the UI #295

Closed

Description

@LarsLaskowski

Summary

Configured base URLs of Docker instances and Portainer are written verbatim into log messages, exception and scan-run error messages, the database and the Docker Instances pages, and the HTTP retry log keeps the user-info part of request URLs. An operator who configures a URL with embedded credentials (https://user:secret@host) therefore leaks that secret to the console log, exported OTLP logs, stored scan-run errors and every visitor of the UI. User info must be stripped or masked everywhere a URL leaves configuration.

Area

  • docs/areas/telemetry.md (added in [Docs] Describe the current behavior by area #263) — Credentials and external data, Open question 3
  • docs/areas/web-ui.md (added in [Docs] Describe the current behavior by area #263) — Secrets, Open question 3
  • docs/areas/docker-instances.md, docs/areas/container-actions.md, docs/areas/registries.md (log and error messages of the clients)
  • Security areas (.squad/project.md): Secrets in configuration (never logged, never shown in the UI, never in exception messages or telemetry attributes) and Logging and telemetry of external data; also touches Registry and Docker Hub calls (TransientHttpRetryHandler) and Container actions (PortainerClient).

Current behavior

Verified locally: new Uri("https://user:secret@portainer.example:9443/api/auth?x=1").GetLeftPart(UriPartial.Path) returns https://user:secret@portainer.example:9443/api/auth; Uri.UserInfo of tcp://user:secret@docker.example:2376 is user:secret. Configuration validation accepts URLs with user info (Configuration/DockerUpdateGuardOptionsValidator.cs:299-305, :327-339).

All paths below are under src/DockerUpdateGuard/.

Logs

  • Infrastructure/TransientHttpRetryHandler.cs:120-135 — GetRequestTarget drops only the query string; GetLeftPart(UriPartial.Path) keeps user info. Used by events 3500/3501 (Infrastructure/TransientHttpRetryHandlerLogging.cs:20-49, {RequestTarget}).
  • Docker/DockerInstanceClientLogging.cs:26-31 event 3101 "Docker instance {DockerInstanceName} uses unsupported endpoint {BaseUrl}", called at Docker/DockerInstanceClient.cs:983 with instanceOptions.BaseUrl.
  • Images/ImageHostLoggingExtensions.cs:612-617 "Docker instance {DockerInstanceName} uses unsupported connection settings for endpoint {BaseUrl}", called at Images/InstanceDiscoveryService.cs:118 with configuredInstance.BaseUrl.
  • Portainer/PortainerClientLogging.cs:25-28 event 3303 "Portainer authentication failed for {BaseUrl}", called at Portainer/PortainerClient.cs:166,173 with options.BaseUrl.

Exception and result messages (end up in logs and, for scans, in ScanRun.ErrorMessage, which the dashboard, scan history and detail pages show)

  • Portainer/PortainerClient.cs:168,175 — InvalidOperationException($"… for '{options.BaseUrl}'").
  • Docker/DockerInstanceClient.cs:985,1063,1152,1189,1223 — ExternalOperationResult<…>.Unsupported($"Docker instance '{…Name}' uses an unsupported endpoint '{instanceOptions.BaseUrl}'").

Database and UI

  • Images/InstanceDiscoveryService.cs:109 stores configuredInstance.BaseUrl.Trim() in DockerInstance.EndpointUri; :155 stores the Portainer BaseUrl in PortainerEndpoint.BaseUrl. Neither stored value is used for connecting (the clients use the configured options); EndpointUri exists for display.
  • UI/ApplicationViewService.cs:2101,2148 map EndpointUri to the view data, rendered at Components/Pages/DockerInstances.razor:31,137 and Components/Pages/DockerInstanceDetail.razor:23.

Reproduction: configure DockerUpdateGuard:DockerInstances:0:Portainer:BaseUrl = https://admin:S3cret@portainer.example:9443 with wrong credentials; on the first Portainer call the log contains Portainer authentication failed for https://admin:S3cret@portainer.example:9443. Configure a Docker instance BaseUrl = https://user:S3cret@docker.example:2376; the Docker Instances page shows the URL including user:S3cret, and a transient 5xx from it logs Transient HTTP response 503 for GET https://user:S3cret@docker.example:2376/v1.41/containers/json; retrying ….

Expected behavior

  • No log message, log attribute, span attribute, exception message, ExternalOperationResult message, scan-run error message, stored display value or UI text contains the user-info part of a URL.
  • Wherever a URL with user info would appear, it appears with the user info replaced by *** (e.g. https://***@portainer.example:9443), so operators can still see that credentials are configured. URLs without user info are shown unchanged (including unix:///var/run/docker.sock and npipe://…).
  • The retry log additionally keeps omitting the query string (unchanged).
  • Strings that are not parseable as absolute URIs but contain a scheme://userinfo@ prefix are masked the same way (the unsupported-endpoint paths log exactly such strings).
  • DockerInstance.EndpointUri and PortainerEndpoint.BaseUrl are stored masked; the UI masks again on display so rows written before the fix (e.g. of instances no longer in configuration, which synchronization does not update) never show credentials.
  • Connecting still uses the configured URL as is (masking is for output only).
  • The OpenTelemetry HTTP client spans (url.full) and the ASP.NET Core spans must not carry user info either (the HTTP client instrumentation is expected to strip it already; verify).

Acceptance criteria

  • One helper (e.g. Infrastructure/UrlRedaction.cs with string RedactUserInfo(string? url) and an overload for Uri) is used at every site listed under Current behavior.
  • Unit tests for the helper (UrlRedactionTests): UrlRedactionRedactUserInfoUserAndPasswordMasked, …UserOnlyMasked, …NoUserInfoUnchanged, …UnixSocketUnchanged, …UnparsableWithUserInfoMasked, …NullOrEmptyReturnsEmpty, …QueryStringPreserved (retry handler removes the query separately).
  • TransientHttpRetryHandlerTests: a retried request to https://user:secret@host/path?token=x logs https://***@host/path (no user info, no query).
  • PortainerClientTests: failed authentication logs and throws without the user info.
  • DockerInstanceClientTests: unsupported endpoint result message and log contain no user info.
  • InstanceDiscoveryServiceTests: stored EndpointUri and PortainerEndpoint.BaseUrl are masked.
  • DockerInstancesRenderTests / ApplicationViewServiceTests: an endpoint stored unmasked (legacy row) is displayed masked.
  • A test (or documented manual check in the PR) confirms that url.full of HTTP client spans contains no user info.
  • Tests use the existing TestLogger/fakes; assertions check the absence of the secret string, with messages.
  • Coverage gate passes; analyzer gate clean.
  • The area documents docs/areas/telemetry.md and docs/areas/web-ui.md describe the new behavior (URLs appear without user info; endpoints are shown masked) and their Known gaps entries for this issue are removed; docs/ARCHITECTURE.md Security posture updated if it states what logs carry.

Implementation notes

  • Prefer Uri.GetComponents(UriComponents.SchemeAndServer | UriComponents.Path, UriFormat.UriEscaped) plus re-inserting ***@ when UserInfo is not empty, over string surgery; fall back to a regex ^([A-Za-z][A-Za-z0-9+.-]*://)[^/@]*@ → $1***@ for unparsable input.
  • Apply masking at the boundary (the log/exception/result call and the store/display mapping), not by changing the options objects — the clients must keep connecting with the configured URL.
  • Masking on write to the database is enough for active instances because synchronization rewrites EndpointUri/BaseUrl on every run (InstanceDiscoveryService.cs:109,155); masking on display covers stale rows. No migration needed.
  • Search for further URL output while implementing (BaseUrl, EndpointUri, RequestUri, GetLeftPart) — the list above is complete as of this analysis, but new sites must use the helper too.
  • Must not change: event IDs and message templates' parameter names (dashboards rely on them; see docs/areas/telemetry.md), the rule that validation messages name keys and instance names only, AllowInsecureHttp/SkipCertificateValidation warnings.
  • This is a security-tier change (secrets in logs/UI): Security reviews the plan and the diff.

Related


Generated by Claude Code

Activity

  1. LarsLaskowski commented on Oct 24, 2026

    @LarsLaskowski
    OwnerAuthor

    Squad working record

    Tier: security. Plan: specs/issue-295/plan.md (permalink to the last commit that contains it). Decision record 0005 (accepted). PR: #305.

    Summary. One helper, UrlRedaction.RedactUserInfo, replaces user info with *** at every output site (retry log, Docker/Portainer logs, exception and result messages, stored EndpointUri/PortainerEndpoint.BaseUrl, UI mapping). It rebuilds a URL only when it has exactly one @, parses, starts with scheme:// and has non-empty user info; every other value containing @ is masked fail-closed up to its last @. This replaces the issue's regex suggestion, which would have leaked forms like https://user:p@ss@host or passwords starting with #, ? or /. Extra sites found beyond the issue: the named-pipe exception message and the stored BaseUrl of a disabled Portainer endpoint. url.full of HTTP client spans carries no user info (checked in the OpenTelemetry and runtime sources). The Docker Engine client has no retry handler, so the issue's Docker retry example cannot occur. Challenge: 0 major, 1 minor (answered). Security plan review: one revision (empty-user-info branch), then approved; Security diff review approved. Follow-ups: #304 (Trivy error output). Squad lessons: #306.

    log.md
    Date Step Member Result
    2026-10-24 10:48 1 Intake Orchestrator bug: label bug; branch claude/nice-curie-340hld off main 4ba3473
    2026-10-24 11:00 2 Plan squad-lead DONE: tier security, plan.md + decision 0005; areas: secrets, logging, retry handler, Portainer, Docker (opus/medium · 203,929 tokens · 68 tool uses · 699 s)
    2026-10-24 11:03 2 Plan challenge squad-devils-advocate 0 major, 1 minor (fact 12: backslash form parses, masked by fallback) (opus/medium · 78,127 tokens · 18 tool uses · 180 s)
    2026-10-24 11:03 3 Plan security review squad-security CHANGES_REQUIRED: B1 empty-UserInfo branch leaks for passwords starting with # ? / (fail-closed fix); N1 relative-target query; N2 re-redaction idempotent (opus/medium · 67,878 tokens · 10 tool uses · 155 s)
    2026-10-24 11:05 2 Plan revise squad-lead DONE: B1 accepted (fail-closed rule, exactly-one-@ rebuild), N1, N2, DA minor answered (opus/medium · 19,964 tokens · 8 tool uses · 99 s · continued)
    2026-10-24 11:07 3 Plan security review (delta) squad-security APPROVED; N3 (fallback scheme prefix check) appended as Security note (opus/medium · 16,351 tokens · 4 tool uses · 62 s · continued)
    2026-10-24 11:11 4 Skeleton squad-dev DONE: UrlRedaction.cs stubs (InvalidOperationException instead of NotImplementedException: RH2003 is an error in Release); build 0 warnings (sonnet/medium · 82,477 tokens · 12 tool uses · 280 s)
    2026-10-24 11:17 5 Tests first squad-tester DONE: 45 of 134 tests fail on skeleton, 89 pass; deferred: ApplicationViewService...KeepAlreadyMaskedEndpointAsync, already-masked row of DisabledPortainer test (sonnet/medium · 156,772 tokens · 30 tool uses · 312 s)
    2026-10-24 11:21 6 Implement squad-dev DONE: all 45 tests green; coverage 92.5% new/84.5% overall; uncovered: relative branch GetRequestTarget, IsValidScheme false return (sonnet/medium · 102,347 tokens · 13 tool uses · 193 s)
    2026-10-24 11:24 6 Coverage squad-tester DONE: relative-URI retry test, 1x:// scheme row; deferred AC8 test green (sonnet/medium · 6,340 tokens · 5 tool uses · 159 s · continued)
    2026-10-24 11:26 7 Code check Orchestrator Format check, Build, Test 570+21 green, Coverage gate PASS (new 100%, overall 84.5%), Analyzer gate PASS, scope-check PASS; Sonar gate NOT CHECKED (no SONAR_TOKEN) -> step 11
    2026-10-24 11:28 8 Review round 1 squad-reviewer APPROVE: no blocking, no non-blocking findings (2 style notes not reported: GetRequestTarget summary, region order) (opus/medium · 84,543 tokens · 18 tool uses · 124 s)
    2026-10-24 11:28 8 Security diff review squad-security APPROVED; non-blocking: Trivy CLI error output not masked (out of scope per 0005) -> follow-up issue as planned (opus/medium · 68,797 tokens · 12 tool uses · 127 s)
    2026-10-24 11:28 9 PR approval Orchestrator approved by checklist; record 0005 Accepted; follow-up issue #304 (Trivy output)
    2026-10-24 11:30 10 Pull request Orchestrator PR #305 opened as draft; working record posted on the issue; follow-up #304
    2026-10-24 11:30 12 Wrap-up Orchestrator lessons: #306 (stack.md skeleton vs RH2003)

    Squad run metrics

    Member Model/effort Launches Tokens Tool uses Seconds
    squad-dev sonnet/medium 2 184,824 25 473
    squad-devils-advocate opus/medium 1 78,127 18 180
    squad-lead opus/medium 2 (1 continued) 223,893 76 798
    squad-reviewer opus/medium 1 84,543 18 124
    squad-security opus/medium 3 (1 continued) 153,026 26 344
    squad-tester sonnet/medium 2 (1 continued) 163,112 35 471
    Total 11 (3 continued) 887,525 198 2390
    Member Wall time (s) Not covered by launches (s) Gate runs
    Orchestrator 2,520 449 1

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingsecuritySecurity issue

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions