fix(registry): resolve relative redirect Location in GetBlobLocationAsync - #423
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #423 +/- ##
==========================================
+ Coverage 93.32% 93.34% +0.01%
==========================================
Files 69 69
Lines 3463 3470 +7
Branches 428 431 +3
==========================================
+ Hits 3232 3239 +7
Misses 139 139
Partials 92 92 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR fixes BlobStore.GetBlobLocationAsync redirect handling to accept relative Location headers by resolving them against the effective request URI (RFC 9110 §10.2.2), aligning blob redirect behavior with existing upload-session redirect handling in PushAsync/MountAsync.
Changes:
- Resolve relative
Locationvalues to an absoluteUribefore returning and before enforcing the HTTPS requirement. - Update XML docs on
BlobStore.GetBlobLocationAsyncandIBlobLocationProvider.GetBlobLocationAsyncto note relative-location resolution and credential implications. - Add focused theory-based test coverage for several relative
Locationshapes (path-absolute, query-only, and protocol-relative) and remove the old “relative location throws” error-case.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/OrasProject.Oras/Registry/Remote/BlobStore.cs |
Resolves relative redirect Location values against the request URI and applies HTTPS validation to the resolved URI. |
src/OrasProject.Oras/Registry/IBlobLocationProvider.cs |
Documents the resolved-relative behavior and that the result may require registry credentials. |
tests/OrasProject.Oras.Tests/Registry/Remote/RepositoryTest.cs |
Adds theory coverage for relative Location redirect behavior and updates the error test to remove the prior “relative throws” expectation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…sync GetBlobLocationAsync threw an HttpIOException when a blob redirect returned a relative Location header. RFC 9110 section 10.2.2 defines Location as a URI-reference and requires resolving a relative reference against the request URI, so rejecting one was stricter than HTTP semantics. Resolve the relative reference against the request URI instead, matching the upload session Location handling already used by PushAsync and MountAsync, and run the HTTPS validation against the resolved URI. This is a behavioral change: a relative Location now yields a resolved URL rather than throwing. A protocol-relative reference such as //storage.example.com/blob resolves to a different host, which is accepted because an absolute cross-host Location was already accepted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Akash Singhal <akashsinghal@microsoft.com>
b54e3bb to
898ae4d
Compare
|
Consider if you want to incorporate this AI FeedbackRecommended non-blocking testsUse two real HTTP endpoints to verify the default authenticated client makes exactly one registry request and never contacts an absolute or scheme-relative redirect target. |
The redirect scheme check only ran when PlainHttp was disabled, so with PlainHttp enabled a Location such as file:///c:/secret, javascript:alert(1), or \\\\host\\share was returned to the caller unvalidated. A UNC path in particular parses as an absolute file:// URI with a foreign host. Require the resolved location to be HTTPS, or HTTP when PlainHttp is enabled. The check stays in the remote BlobStore rather than the IBlobLocationProvider contract, so a local or third-party implementation remains free to return other schemes. Also add tests that the redirect target is never contacted (exactly one request, to the registry blob endpoint), and pin explicit port and malformed relative Location handling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Akash Singhal <akashsinghal@microsoft.com>
|
Thanks — I dug into each item. Took three, skipping the rest with reasoning below. Added in cb5511a1. Scheme allowlist — this found a real gap. The check was
The resolved location must now be 2. Redirect target is never contacted. 3. Explicit port and malformed values pinned in the theory: Skipped, with reasons
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/OrasProject.Oras/Registry/Remote/BlobStore.cs:365
- The invalid-scheme error message formats the rejected URI as
{scheme}://{host}, which drops important details like the port (and can be misleading for non-authority schemes). Including the full resolved Location URI makes diagnostics more accurate.
var expectedSchemes = Repository.Options.PlainHttp ? "HTTP or HTTPS" : "HTTPS";
throw new HttpIOException(HttpRequestError.InvalidResponse,
$"{response.RequestMessage?.Method} {response.RequestMessage?.RequestUri}: " +
$"redirect location must use {expectedSchemes}, " +
$"got {blobLocation.Scheme}://{blobLocation.Host}");
tests/OrasProject.Oras.Tests/Registry/Remote/RepositoryTest.cs:1326
- This assertion only checks for the substring "HTTPS" even when
plainHttpis true (where the expected message is "HTTP or HTTPS"). That makes the test less precise and could miss regressions in the PlainHttp branch.
var exception = await Assert.ThrowsAsync<HttpIOException>(async () =>
await store.GetBlobLocationAsync(blobDesc, new CancellationToken()));
Assert.Contains("HTTPS", exception.Message);
}
|
Extra context on why this matters in practice: relative That makes this less a spec-conformance cleanup and more an interoperability fix — resolving per RFC 9110 §10.2.2 is what lets us work with the registries that are already doing this. |
A malformed Location such as ht!tp://[bad has no legal scheme, so it parses as a relative reference and was resolved into a bogus URL on the registry host rather than being rejected. Validate the Location with IsWellFormedOriginalString before resolving, matching the existing Link header convention in HttpResponseMessageExtensions. Realistic pre-signed URLs (Azure SAS, S3, GCS) are unaffected; only genuinely malformed values, such as unescaped brackets, spaces, or a UNC path, are rejected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Akash Singhal <akashsinghal@microsoft.com>
|
One more consistency pass (4a6c2a2). The Checked that this does not reject anything real — Azure SAS, S3 pre-signed (including The resolution itself is unchanged and remains identical to the existing upload session handling in |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/OrasProject.Oras/Registry/Remote/BlobStore.cs:361
- Relative Location resolution should use the effective request URI (
response.RequestMessage.RequestUri) as the base rather than the precomputedurl. This matches RFC 9110 wording (“effective request URI”) and is consistent withHttpResponseMessageExtensions.ParseLink()which resolves relative header values againstresponse.RequestMessage.RequestUri. It also avoids subtle mismatches if a handler normalizes or rewrites the request URI before sending.
var blobLocation = location.IsAbsoluteUri ? location : new Uri(url, location);
What this PR does / why we need it
BlobStore.GetBlobLocationAsyncrejected redirect responses whoseLocationheader was a relative reference, throwingHttpIOException("redirect Location header must be an absolute URI").That was stricter than HTTP semantics. RFC 9110 §10.2.2 defines
Location = URI-referenceand requires a relative reference to be resolved against the effective request URI. The OCI Distribution Spec v1.1.1 does not describe blob-pull redirects at all (it has no mention of redirects or any 3xx status code), but it does explicitly permit a relativeLocationfor upload sessions, citing RFC 7231 §7.1.2 — and this library already resolves those inPushAsyncandMountAsync.This PR makes the blob redirect path consistent with both:
The base is the request URI built earlier in the method, which the existing auto-redirect guard already proves equal to
response.RequestMessage.RequestUri. HTTPS validation now runs against the resolved URI.Behavioral change
A relative
Locationnow returns a resolved URL instead of throwing. No API signatures change, so no major version bump appears necessary, but the error-path behavior change is worth a release note.Three consequences worth reviewer attention:
Location, because a resolved relative reference always inherits the registry's scheme. It still guards absolutehttp://locations.//storage.example.com/blobreportsIsAbsoluteUri == falseyet resolves to a different host, so it is now accepted where it previously threw. This is not a new capability for a hostile registry, which could already return an absolutehttps://evil.example.com/blob; restricting only the relative branch to same-origin would be asymmetric and would reintroduce the RFC non-conformance. The behavior is pinned by a test rather than left implicit.BlobStoreandIBlobLocationProvidernow state this.Tests
The relative-URI case moved out of
BlobStore_GetBlobLocationAsync_Errorsinto a new[Theory] BlobStore_GetBlobLocationAsync_RelativeLocation:LocationPlainHttp/storage/blobs/testtruehttp://localhost:5000/storage/blobs/test/storage/blobs/testfalsehttps://localhost:5000/storage/blobs/test//storage.example.com/blobfalsehttps://storage.example.com/blob?token=xyzfalsehttps://localhost:5000/v2/test/blobs/{digest}?token=xyzThe last two cover the cross-host form discussed above and the pre-signed-token form registries commonly emit. Existing negative cases (missing
Location, non-HTTPS absolute location, 404) are unchanged. Full suite: 637 passing.Follow-up: HTTP(S) scheme validation
Per review feedback, the scheme check previously ran only when
PlainHttpwas disabled, so withPlainHttp = trueaLocationoffile:///c:/secret,javascript:alert(1), or\\host\share(which parses as an absolutefile://URI with a foreign host) was returned to the caller unvalidated. The resolved location must now be HTTPS, or HTTP whenPlainHttpis enabled. The check lives in the remoteBlobStorerather than theIBlobLocationProvidercontract, so a local or third-party implementation remains free to return other schemes.Additional tests: the redirect target is never contacted (exactly one request, to the registry blob endpoint), non-HTTP(S) schemes are rejected in both
PlainHttpmodes, and explicit ports plus malformed relative values are pinned.Which issue(s) this PR resolves / fixes
N/A
Please check the following list
BlobStore.GetBlobLocationAsyncandIBlobLocationProvider.GetBlobLocationAsyncupdated.