Steps to reproduce
-FollowRelLink exists to page authenticated REST APIs, GitHub's and GitLab's being the canonical examples. As of the 2026-08-14 servicing releases — 7.4.19, 7.5.10 and 7.6.5 — the cmdlet sends the caller's Authorization header on the first request only; every page reached by following a rel="next" link is fetched anonymously.
Against a server that permits anonymous reads, those requests still succeed. They just return a smaller, visibility-filtered result set, with HTTP 200 and no warning, so the caller receives silently partial data rather than an error. (Against a fully private server the follow fails loudly with 401/404 instead, which is presumably why this hasn't been widely reported yet.)
What changed. One change, landed on each servicing line through its own Azure DevOps PR — "Strip authorization on redirect if -PreserveAuthorizationOnRedirect is not specified". On 7.6 that is commit e209aea (PR 41050), which changed WebRequestPSCmdlet.Common.cs#L570:
- using (HttpRequestMessage request = GetRequest(uri))
+ using (HttpRequestMessage request = GetRequest(uri, isRedirect: followedRelLink > 0))
GetRequest then drops the header:
if (isRedirect && !PreserveAuthorizationOnRedirect && entry.Key is HttpKnownHeaderNames.Authorization)
{
continue;
}
Impacted Versions
| Released version |
Affected |
Rel-link call site |
Introducing commit |
| v7.4.19 (LTS) |
yes |
L565 |
d00974dc7 — PR 41040 |
| v7.5.10 |
yes |
L565 |
0c59ffd11 — PR 41045 |
| v7.6.5 |
yes |
L570 |
e209aea1d — PR 41050 |
| v7.4.18, v7.5.9, v7.6.4 and earlier |
no |
— |
— |
| 7.7.0-preview.x, 7.9.x-preview |
no |
— |
— |
So anyone paging an authenticated API is affected on whichever line they upgraded to in August, LTS included.
Pending releases that would carry it forward.
| Target |
Carries it today |
Effect |
| release/v7.4.20 |
yes |
the next 7.4 LTS patch ships with it unless the call site changes |
| next 7.5 and 7.6 patches |
yes, inherited from v7.5.10 / v7.6.5 |
no public branch for either at the moment |
| master, and every preview branch |
no |
GetRequest(uri) there takes no isRedirect argument |
That last row is the one worth planning around. master is clean only because this change has not been forward-ported yet — it went straight onto the servicing branches. Whenever it does move forward it carries the rel-link call site with it, and 7.7 and later inherit the same behavior. Whatever shape the fix takes on the servicing lines needs to ride along with that port.
I recognize this looks deliberate — the same commit added a test asserting it:
Validate Invoke-RestMethod -FollowRelLink strips the authorization header on followed relation links by default
So the question isn't whether the code does what it was written to do. It's whether the cost to authenticated pagination was weighed when redirect semantics were extended to -FollowRelLink.
The argument for treating them differently: a rel-link follow 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 them when crossing an origin, not on every hop. Applied unconditionally here, it removes credentials the caller supplied deliberately for the API being paged, which leaves -FollowRelLink unable to serve the use case it was added for.
Repro — self-contained, no external service, runs anywhere pwsh runs. It serves three linked pages from HttpListener and has each page report whether the request that fetched it carried an Authorization header.
# Standalone repro: -FollowRelLink drops the Authorization header on followed links.
# No external service required; serves three linked pages from HttpListener on localhost.
# Each page reports whether the request that fetched it carried an Authorization header.
$port = 8802
$prefix = "http://localhost:$port/"
$listener = Start-ThreadJob -ArgumentList $prefix -ScriptBlock {
param($prefix)
$l = [System.Net.HttpListener]::new()
$l.Prefixes.Add($prefix)
$l.Start()
for ($i = 1; $i -le 3; $i++) {
$ctx = $l.GetContext()
$page = 1
if ($ctx.Request.Url.Query -match 'page=(\d+)') { $page = [int]$Matches[1] }
$auth = $ctx.Request.Headers['Authorization']
$body = @{ page = $page; authorization = $auth } | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($body)
if ($page -lt 3) {
$next = $prefix.TrimEnd('/') + "/?page=$($page + 1)"
$ctx.Response.AddHeader('Link', "<$next>; rel=`"next`"")
}
$ctx.Response.ContentType = 'application/json'
$ctx.Response.ContentLength64 = $bytes.Length
$ctx.Response.OutputStream.Write($bytes, 0, $bytes.Length)
$ctx.Response.Close()
}
$l.Stop()
}
Start-Sleep -Milliseconds 700
$result = Invoke-RestMethod -Uri "$prefix`?page=1" -Headers @{ Authorization = 'test' } -FollowRelLink -MaximumFollowRelLink 10
Write-Host "PowerShell $($PSVersionTable.PSVersion)"
foreach ($r in $result) {
$seen = if ([string]::IsNullOrEmpty($r.authorization)) { '<none>' } else { $r.authorization }
Write-Host (" page {0}: server saw Authorization = {1}" -f $r.page, $seen)
}
$null = Receive-Job $listener -Wait -ErrorAction SilentlyContinue
Remove-Job $listener -Force -ErrorAction SilentlyContinue
Impact against a real API. On a self-managed GitLab 18.11.6-ee group holding 43 projects (39 public, 4 internal), Invoke-RestMethod -FollowRelLink returned exactly 39 at every page size tried (per_page 5, 10, and 20): page 1 came back authenticated, every later page anonymous. The same call on 7.6.4, same machine and endpoint, returns 43. It reached us through the GitlabCli module, where Get-GitlabProject -GroupId <group> -Recurse -All silently returned 39 of 43 projects.
-PreserveAuthorizationOnRedirect does restore the full result, but it is the same switch that governs genuine cross-origin redirects, so it isn't a safe thing for a shared module to turn on just to page an API.
Remedies, in the order we'd prefer them. Sketches against the v7.6.5 source to make the ask concrete — untested, and offered as a direction rather than a patch. Happy to open a PR for whichever shape you'd accept.
1. Strip only when the followed link leaves the origin. This keeps the security fix's intent for the case it was written for, and restores authenticated pagination for the same-origin case that is essentially all of it:
int followedRelLink = 0;
Uri uri = Uri;
+ string originAuthority = uri.GetLeftPart(UriPartial.Authority);
do
{
@@
- using (HttpRequestMessage request = GetRequest(uri, isRedirect: followedRelLink > 0))
+ // A rel-link follow targets a URL the same server advertised in its own Link
+ // header. Treat it as a credential boundary only when it leaves that origin.
+ bool relLinkLeavesOrigin = followedRelLink > 0
+ && !string.Equals(
+ originAuthority,
+ uri.GetLeftPart(UriPartial.Authority),
+ StringComparison.OrdinalIgnoreCase);
+
+ using (HttpRequestMessage request = GetRequest(uri, isRedirect: relLinkLeavesOrigin))
2. If the unconditional strip stays, make it audible. The failure is silent in both halves — the header vanishes without comment, and the paging loop then exits on a bare return — so nothing distinguishes a truncated read from a complete one:
uri = new Uri(_relationLink["next"]);
followedRelLink++;
+
+ if (!PreserveAuthorizationOnRedirect
+ && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization))
+ {
+ WriteWarning(WebCmdletStrings.AuthorizationStrippedOnRelLink);
+ }
(As written that warns once per followed page; hoisting it to fire once per command would be quieter.)
3. Either way, document the behavior change. None of the three published release notes — 7.4.19, 7.5.10, 7.6.5 — mention authorization or redirects; they list a pwsh -file fix, test and CI changes, and packaging updates. There is no signal connecting an upgrade to newly-truncated results.
Expected behavior
PowerShell 7.6.4
page 1: server saw Authorization = test
page 2: server saw Authorization = test
page 3: server saw Authorization = test
Actual behavior
PowerShell 7.6.5
page 1: server saw Authorization = test
page 2: server saw Authorization = <none>
page 3: server saw Authorization = <none>
Error details
None. That is the substance of the report: the followed requests succeed, so the caller gets a well-formed short result with no error, no warning, and no non-zero status.
Environment data
Name Value
---- -----
PSVersion 7.6.5
PSEdition Core
GitCommitId 7.6.5
OS macOS 26.6.1
Platform Unix
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion 2.4
SerializationVersion 1.1.0.1
WSManStackVersion 3.0
Reproduced on macOS 26.6.1 arm64. Confirmed absent on 7.6.4 on the same machine against the same endpoints.
Steps to reproduce
-FollowRelLink exists to page authenticated REST APIs, GitHub's and GitLab's being the canonical examples. As of the 2026-08-14 servicing releases — 7.4.19, 7.5.10 and 7.6.5 — the cmdlet sends the caller's Authorization header on the first request only; every page reached by following a rel="next" link is fetched anonymously.
Against a server that permits anonymous reads, those requests still succeed. They just return a smaller, visibility-filtered result set, with HTTP 200 and no warning, so the caller receives silently partial data rather than an error. (Against a fully private server the follow fails loudly with 401/404 instead, which is presumably why this hasn't been widely reported yet.)
What changed. One change, landed on each servicing line through its own Azure DevOps PR — "Strip authorization on redirect if -PreserveAuthorizationOnRedirect is not specified". On 7.6 that is commit e209aea (PR 41050), which changed WebRequestPSCmdlet.Common.cs#L570:
GetRequest then drops the header:
Impacted Versions
So anyone paging an authenticated API is affected on whichever line they upgraded to in August, LTS included.
Pending releases that would carry it forward.
That last row is the one worth planning around. master is clean only because this change has not been forward-ported yet — it went straight onto the servicing branches. Whenever it does move forward it carries the rel-link call site with it, and 7.7 and later inherit the same behavior. Whatever shape the fix takes on the servicing lines needs to ride along with that port.
I recognize this looks deliberate — the same commit added a test asserting it:
So the question isn't whether the code does what it was written to do. It's whether the cost to authenticated pagination was weighed when redirect semantics were extended to -FollowRelLink.
The argument for treating them differently: a rel-link follow 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 them when crossing an origin, not on every hop. Applied unconditionally here, it removes credentials the caller supplied deliberately for the API being paged, which leaves -FollowRelLink unable to serve the use case it was added for.
Repro — self-contained, no external service, runs anywhere pwsh runs. It serves three linked pages from HttpListener and has each page report whether the request that fetched it carried an Authorization header.
Impact against a real API. On a self-managed GitLab 18.11.6-ee group holding 43 projects (39 public, 4 internal), Invoke-RestMethod -FollowRelLink returned exactly 39 at every page size tried (per_page 5, 10, and 20): page 1 came back authenticated, every later page anonymous. The same call on 7.6.4, same machine and endpoint, returns 43. It reached us through the GitlabCli module, where Get-GitlabProject -GroupId <group> -Recurse -All silently returned 39 of 43 projects.
-PreserveAuthorizationOnRedirect does restore the full result, but it is the same switch that governs genuine cross-origin redirects, so it isn't a safe thing for a shared module to turn on just to page an API.
Remedies, in the order we'd prefer them. Sketches against the v7.6.5 source to make the ask concrete — untested, and offered as a direction rather than a patch. Happy to open a PR for whichever shape you'd accept.
1. Strip only when the followed link leaves the origin. This keeps the security fix's intent for the case it was written for, and restores authenticated pagination for the same-origin case that is essentially all of it:
int followedRelLink = 0; Uri uri = Uri; + string originAuthority = uri.GetLeftPart(UriPartial.Authority); do { @@ - using (HttpRequestMessage request = GetRequest(uri, isRedirect: followedRelLink > 0)) + // A rel-link follow targets a URL the same server advertised in its own Link + // header. Treat it as a credential boundary only when it leaves that origin. + bool relLinkLeavesOrigin = followedRelLink > 0 + && !string.Equals( + originAuthority, + uri.GetLeftPart(UriPartial.Authority), + StringComparison.OrdinalIgnoreCase); + + using (HttpRequestMessage request = GetRequest(uri, isRedirect: relLinkLeavesOrigin))2. If the unconditional strip stays, make it audible. The failure is silent in both halves — the header vanishes without comment, and the paging loop then exits on a bare return — so nothing distinguishes a truncated read from a complete one:
uri = new Uri(_relationLink["next"]); followedRelLink++; + + if (!PreserveAuthorizationOnRedirect + && WebSession.Headers.ContainsKey(HttpKnownHeaderNames.Authorization)) + { + WriteWarning(WebCmdletStrings.AuthorizationStrippedOnRelLink); + }(As written that warns once per followed page; hoisting it to fire once per command would be quieter.)
3. Either way, document the behavior change. None of the three published release notes — 7.4.19, 7.5.10, 7.6.5 — mention authorization or redirects; they list a pwsh -file fix, test and CI changes, and packaging updates. There is no signal connecting an upgrade to newly-truncated results.
Expected behavior
Actual behavior
Error details
None. That is the substance of the report: the followed requests succeed, so the caller gets a well-formed short result with no error, no warning, and no non-zero status.
Environment data
Reproduced on macOS 26.6.1 arm64. Confirmed absent on 7.6.4 on the same machine against the same endpoints.