Strip auth on rel links only when they leave the origin - #27862
Open
Chris Peterson (chris-peterson) wants to merge 1 commit into
Open
Strip auth on rel links only when they leave the origin#27862Chris Peterson (chris-peterson) wants to merge 1 commit into
Chris Peterson (chris-peterson) wants to merge 1 commit into
Conversation
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: There may be pipelines that require an authorized user to comment /azp run to run. |
Chris Peterson (chris-peterson)
marked this pull request as ready for review
August 18, 2026 00:11
Chris Peterson (chris-peterson)
requested review from
a team and
Justin Chung (jshigetomi)
as code owners
August 18, 2026 00:11
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
There was a problem hiding this comment.
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
LinkControllercan emit arel="next"link targeting a different origin via anextauthorityquery parameter. Invoke-RestMethod -FollowRelLinknow 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; |
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR Summary
Fixes #27861.
-FollowRelLinkexists so web cmdlets can page an API that advertises its next page in aLinkheader — GitHub's and GitLab's paginated endpoints are the canonical case, and those APIs need the caller'sAuthorizationheader on every page. Since e209aea ("Strip authorization on redirect if-PreserveAuthorizationOnRedirectis not specified") the rel-link loop passesisRedirect: followedRelLink > 0, soGetRequestdrops 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
Linkheader, 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-FollowRelLinkunable 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
-PreserveAuthorizationOnRedirectstill overrides both.release/v7.5.11carries 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 frommaster, which has noisRedirectparameter at all, so there is nothing to fix there and nomasterPR to open; the origin check should ride along whenever this change ports forward.Review guide
Start here — the fix.
WebRequestPSCmdlet.Common.csL558-562 captures the requested origin once; L575-578 is the per-iteration comparison feedingisRedirect.CheckProtocolis 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 normalizationGetRequestapplies viaPrepareUri, so passing the normalized URI on is a no-op for the request itself. Every rel link is already absolute, coming fromnew Uri(_relationLink["next"]).The contract change.
WebCmdlets.Tests.ps1L3032 is e209aea's own-FollowRelLinkassertion, 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-PreserveAuthorizationOnRedirectstill overrides across origins.The other four tests e209aea touched (the
-PreserveHttpMethodOnRedirectpairs for both cmdlets) are untouched and pass.Test-tool support.
LinkController.csL81-90 adds anextauthorityquery param so arel="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.ps1on macOS arm64, verified red then green:…keeps the authorization header on relation links within the origin, page 2 arriving with noAuthorizationThe repro from #27861 (a standalone
HttpListenerserving three linked pages, no external service) printsteston all three pages against the fixed build, matching its 7.6.4 output.Build note for anyone reproducing this locally
dotnet restoreon this branch fails against the repo's single NuGet source — the PowerShell Azure DevOps feed returns401 Unauthorized - No local versions of package 'microsoft.netcore.app.runtime.osx-arm64'for10.0.11, which is public on nuget.org. Adding nuget.org with apackageSourceMappingforMicrosoft.NETCore.App.Runtime.*andMicrosoft.AspNetCore.App.Runtime.*gets a clean restore. Not part of this PR.PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header-FollowRelLink; the strip it narrows shipped in v7.6.5.