Skip to content

Strip auth on rel links only when they leave the origin - #27862

Open
Chris Peterson (chris-peterson) wants to merge 1 commit into
PowerShell:release/v7.6.6from
chris-peterson:fix-followrellink-auth-header
Open

Strip auth on rel links only when they leave the origin#27862
Chris Peterson (chris-peterson) wants to merge 1 commit into
PowerShell:release/v7.6.6from
chris-peterson:fix-followrellink-auth-header

Conversation

@chris-peterson

@chris-peterson Chris Peterson (chris-peterson) commented Aug 18, 2026

Copy link
Copy Markdown

PR Summary

Fixes #27861.

-FollowRelLink exists so web cmdlets can page an API that advertises its next page in a Link header — GitHub's and GitLab's paginated endpoints are the canonical case, and those APIs need the caller's Authorization header on every page. Since e209aea ("Strip authorization on redirect if -PreserveAuthorizationOnRedirect is not specified") the rel-link loop passes isRedirect: followedRelLink > 0, so GetRequest drops that header on every page after the first.

The result is silent truncation rather than an error: against a server that permits anonymous reads, the un-authenticated follows still return HTTP 200, just with a visibility-filtered result set. The reporter's GitLab group of 43 projects came back as 39 at every page size, through Get-GitlabProject -Recurse -All, with no warning and no non-zero status.

PR Context

A rel link is not a redirect. It is a client-initiated GET to a URL the same server advertised in its own Link header, and in practice it is same-origin. The conventional rule for credential stripping is to drop at an origin boundary, not on every hop — applied on every hop it removes credentials the caller supplied deliberately for the API being paged, which leaves -FollowRelLink unable to serve the case it was added for.

This compares the followed link's scheme, host and port against the origin the caller requested, and strips only when they differ. Genuine 3xx redirects are untouched, and -PreserveAuthorizationOnRedirect still overrides both.

release/v7.5.11 carries the identical call site and needs the same change. The bug shipped in v7.5.10 and v7.6.5 — the sibling commit is 0c59ffd (Merged PR 41045: [release/v7.5.10] …). It is absent from master, which has no isRedirect parameter at all, so there is nothing to fix there and no master PR to open; the origin check should ride along whenever this change ports forward.

Review guide

Start here — the fix. WebRequestPSCmdlet.Common.cs L558-562 captures the requested origin once; L575-578 is the per-iteration comparison feeding isRedirect.

CheckProtocol is what makes the comparison safe for a scheme-less -Uri (the "URI without scheme" case in the existing rel-link tests) — it is the same normalization GetRequest applies via PrepareUri, so passing the normalized URI on is a no-op for the request itself. Every rel link is already absolute, coming from new Uri(_relationLink["next"]).

The contract change. WebCmdlets.Tests.ps1 L3032 is e209aea's own -FollowRelLink assertion, inverted: WebListener is same-origin, so what it pinned was the case this PR argues is wrong. Two cases replace it at L3040 and L3054 — a cross-origin link still strips, and -PreserveAuthorizationOnRedirect still overrides across origins.

The other four tests e209aea touched (the -PreserveHttpMethodOnRedirect pairs for both cmdlets) are untouched and pass.

Test-tool support. LinkController.cs L81-90 adds a nextauthority query param so a rel="next" can point at another listener port. WebListener generated every link from the request's own display URL, so nothing in this suite could reach a second origin — the redirect tests included, which is why "strips on redirect" was only ever proven same-origin.

Testing

WebCmdlets.Tests.ps1 on macOS arm64, verified red then green:

Build Result
Fix reverted, tests present 612 passed, 1 failed…keeps the authorization header on relation links within the origin, page 2 arriving with no Authorization
Fix applied 613 passed, 0 failed, 17 skipped, 36 pending

The repro from #27861 (a standalone HttpListener serving three linked pages, no external service) prints test on all three pages against the fixed build, matching its 7.6.4 output.

Build note for anyone reproducing this locally

dotnet restore on this branch fails against the repo's single NuGet source — the PowerShell Azure DevOps feed returns 401 Unauthorized - No local versions of package 'microsoft.netcore.app.runtime.osx-arm64' for 10.0.11, which is public on nuget.org. Adding nuget.org with a packageSourceMapping for Microsoft.NETCore.App.Runtime.* and Microsoft.AspNetCore.App.Runtime.* gets a clean restore. Not part of this PR.

PR Checklist

A rel link is a URL the same server advertised in its own Link header,
reached by a client-initiated GET. Routing it through the redirect path
dropped the Authorization header the caller supplied for that API, so
-FollowRelLink fetched every page after the first anonymously. Against a
server that permits anonymous reads the pages still return 200, and the
caller gets a visibility-filtered result set with no error and no
warning.

Compare the followed link's scheme, host and port against the origin the
caller requested, and strip only when they differ. Cross-origin links
keep the redirect treatment, and -PreserveAuthorizationOnRedirect still
overrides both.

The same call site is present on release/v7.5.11 and shipped in v7.5.10,
so the 7.5 line needs the same change.

Refs: PowerShell#27861
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@chris-peterson
Chris Peterson (chris-peterson) marked this pull request as ready for review August 18, 2026 00:11
Copilot AI lite review requested due to automatic review settings August 18, 2026 00:11
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds cross-origin coverage for -FollowRelLink and updates logic so Authorization is preserved for same-origin rel links, while being stripped when a rel link leaves the origin (unless explicitly preserved).

Changes:

  • WebListener LinkController can emit a rel="next" link targeting a different origin via a nextauthority query parameter.
  • Invoke-RestMethod -FollowRelLink now treats followed rel links as a credential boundary only when they leave the original origin.
  • Updates/adds Pester tests to validate Authorization preservation within-origin and stripping across-origin, plus opt-in preservation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
test/tools/WebListener/Controllers/LinkController.cs Adds nextauthority support to generate a cross-origin next link for test scenarios.
test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 Splits/extends tests to cover same-origin vs cross-origin Authorization behavior and explicit preservation.
src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs Changes rel-link follow behavior to strip Authorization only when leaving the originally requested origin.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

// so the credentials the caller supplied still apply. Treat a followed link as a credential
// boundary only when it leaves the origin the caller requested.
Uri uri = CheckProtocol(Uri);
string originAuthority = uri.GetLeftPart(UriPartial.Authority);
Comment on lines +575 to +578
bool relLinkLeavesOrigin = followedRelLink > 0
&& !string.Equals(originAuthority, uri.GetLeftPart(UriPartial.Authority), StringComparison.OrdinalIgnoreCase);

using (HttpRequestMessage request = GetRequest(uri, isRedirect: relLinkLeavesOrigin))
// A rel link is a URL the same server advertised in its own 'Link' header, not a redirect,
// so the credentials the caller supplied still apply. Treat a followed link as a credential
// boundary only when it leaves the origin the caller requested.
Uri uri = CheckProtocol(Uri);
bool relLinkLeavesOrigin = followedRelLink > 0
&& !string.Equals(originAuthority, uri.GetLeftPart(UriPartial.Authority), StringComparison.OrdinalIgnoreCase);

using (HttpRequestMessage request = GetRequest(uri, isRedirect: relLinkLeavesOrigin))
Comment on lines +84 to +87
if (Request.Query.TryGetValue("nextauthority", out StringValues nextAuthoritySV)
&& Uri.TryCreate(nextAuthoritySV.FirstOrDefault(), UriKind.Absolute, out Uri nextAuthority))
{
nextBaseUri = new Uri(nextAuthority, new Uri(baseUri).AbsolutePath).AbsoluteUri;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants