Skip to content

fix: safely follow cross-host S3 redirects - #8205

Closed
anonx3247 wants to merge 1 commit into
lance-format:mainfrom
anonx3247:fix/s3-redirect-host
Closed

fix: safely follow cross-host S3 redirects#8205
anonx3247 wants to merge 1 commit into
lance-format:mainfrom
anonx3247:fix/s3-redirect-host

Conversation

@anonx3247

@anonx3247 anonx3247 commented Aug 3, 2026

Copy link
Copy Markdown

Summary

  • add an opt-in S3 connector for credential-safe cross-authority Range GET redirects
  • regenerate Host for the redirected authority without replacing Lance's metrics/AIMD connector
  • reject credential configurations and headers that reqwest would otherwise forward to the redirect target

Implementation

The preferred signing-owner fix exists in object_store 0.14.1, but Lance cannot upgrade yet because object_store_opendal 0.58 requires object_store ^0.13.1 and no compatible adapter release is available. This revision therefore applies the equivalent Host correction at Lance's composed HTTP connector boundary while retaining object_store 0.13.2.

The behavior is opt-in through s3_allow_credential_safe_redirects=true and applies only to GET requests. Guarded mode accepts static AWS credentials without custom headers.* values. It rejects S3 Express, AWS session tokens, SSE-C customer keys, and custom gateway headers rather than risk forwarding those credentials across authorities.

Verification

  • cargo test -p lance-io --features aws,metrics object_store::providers::aws::tests
  • cargo test -p lance-io --no-default-features --features aws storage_option_ (pre-existing unused-variable warning outside this diff)
  • cargo clippy -p lance-io --all-features --tests --benches -- -D warnings
  • cargo clippy --all --tests --benches -- -D warnings
  • cargo fmt --all -- --check

@github-actions github-actions Bot added A-python Python bindings A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-deps Dependency updates A-encoding Encoding, IO, file reader/writer A-format On-disk format: protos and format spec docs labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Important

This PR touches the Lance format specification.

Substantive changes to the format specification — the .proto definitions
and the spec docs under docs/src/format/ — require a PMC vote before merge.
Minor edits such as typo fixes, wording, or formatting are excluded; use your
judgment.

If this is a meaningful format change:

  • Start a vote following the Lance community voting process.
    Format specification modifications need 3 binding +1 votes (excluding the
    proposer), held on GitHub Discussions, with a minimum voting period of 1 week.
  • Once the vote passes, link the completed vote in this PR. It should not be
    merged until the vote is linked.

@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026

@lance-gatekeeper lance-gatekeeper Bot 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.

Gate recommendation: request changes.

The underlying Host bug is real, but this revision does not preserve the security and current-main integration contracts required for cross-authority S3 redirects. It also carries unrelated 8.0 release ancestry.

Prefer a current-main revision that fixes Host at the signing owner (as object_store 0.14.1 does) and separately implements an explicit credential-safe redirect policy before forwarding Range GETs. Compose the HTTP layers so metrics and redirect sanitation both remain active, and exercise the storage option through an end-to-end signed S3 test.

#[async_trait::async_trait]
impl HttpService for RebuildRedirectHostService {
async fn call(&self, mut request: HttpRequest) -> std::result::Result<HttpResponse, HttpError> {
request.headers_mut().remove(http::header::HOST);

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.

Cross-authority redirects still forward AWS credential headers that reqwest does not classify as Authorization. object_store 0.13.2 inserts x-amz-security-token for temporary credentials and can attach x-amz-server-side-encryption-customer-key to GETs; reqwest strips neither on a host change. A redirect target can therefore receive a session token or plaintext SSE-C key.

The redirect layer must either reject such cross-authority redirects or construct a new unsigned request containing only safe headers such as Range; stripping Host before delegating to reqwest is not a complete credential boundary.

Reproducer

I augmented this test request and asserted both values were absent from destination_request:

.header("x-amz-security-token", "temporary-session-secret")
.header(
    "x-amz-server-side-encryption-customer-key",
    "customer-encryption-secret",
)

Then I ran:

cargo test -p lance-io --features aws object_store::providers::aws::tests::test_redirect_rebuilds_host_and_preserves_range -- --exact

Expected: neither secret reaches the destination. Observed: the test failed on the session token; after checking the SSE-C assertion first, it failed on the customer key as well.

@anonx3247 anonx3247 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e2c32f1. The opt-in connector now rejects GETs containing the x-amz-security-token header or any SSE-C customer-key header before network dispatch. The storage-option regression test covers both temporary session credentials and SSE-C, and the successful redirect test proves Authorization is stripped while Range is preserved.

.get(REBUILD_REDIRECT_HOST_OPTION)
.is_some_and(|value| value == "true")
{
builder = builder.with_http_connector(RebuildRedirectHostConnector);

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.

This connector is overwritten when the change is applied to the live base with metrics enabled. Current main later calls with_http_connector(MeteringHttpConnector::new(...)), and AmazonS3Builder::with_http_connector replaces its single connector slot. Python and Java both enable lance-io/metrics, so this option becomes a no-op in those bindings.

I reproduced the integration by cherry-picking this S3 commit alone onto d5050ad; the combined function installs this connector first and the metering connector last. Please compose the services behind one connector and test the storage-option path with metrics enabled.

@anonx3247 anonx3247 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e2c32f1. The redirect guard now wraps cloud_http_connector, so the existing AIMD and MeteringHttpConnector layers remain active instead of being overwritten. The end-to-end storage-option regression runs with the aws and metrics features enabled.

Comment thread .bumpversion.toml Outdated
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "8.0.0-beta.19"
current_version = "8.0.0"

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.

This line comes from four unrelated 8.0 release/FM-index commits in the PR range. The live base is 10.1.0-beta.2, while git log d5050ad..d7bbc7a contains five commits and the three-dot diff spans 13 files. A three-way merge experiment produced release-version conflicts across the root, Python, and Java manifests.

Please rebase on current main or cherry-pick only the S3 redirect change so this PR contains no release or format/index payload.

@anonx3247 anonx3247 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e2c32f1. The branch was rebuilt on current upstream main; the PR now contains one commit changing only rust/lance-io/src/object_store/providers/aws.rs, with no release, protobuf, FM-index, Python, or Java payload.

@anonx3247 anonx3247 changed the title fix: rebuild Host across S3 redirects fix: safely follow cross-host S3 redirects Aug 5, 2026
@anonx3247
anonx3247 force-pushed the fix/s3-redirect-host branch from d7bbc7a to e2c32f1 Compare August 5, 2026 18:24

@lance-gatekeeper lance-gatekeeper Bot 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.

Gate recommendation: request changes.

The earlier Host, branch-scope, and metrics/AIMD issues are resolved, but this opt-in still does not establish a complete cross-authority credential boundary.

Prefer rebuilding redirected GETs from an explicit allowlist of safe headers, or rejecting every credential-bearing configuration that cannot be proven safe; a small sensitive-header blacklist cannot cover the credential sources already supported by this S3 path.

return self.inner.execute(request).await;
}
if let Some(header) = request.headers().keys().find(|header| {
header.as_str() == "x-amz-security-token"

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.

This blacklist still forwards two supported credential sources to a different authority. S3 Express signs GETs with x-amz-s3session-token, not x-amz-security-token, and Lance's headers.* storage options can add arbitrary gateway credentials such as x-api-key; reqwest strips neither header on a cross-host redirect. A redirect target can therefore receive either the S3 Express session token or the gateway secret.

Please make the cross-authority path forward only an explicit set of safe GET headers, or reject S3 Express and every non-allowlisted custom header when this option is enabled.

Reproducers

On this head I added disposable local 307 regressions. The first sent x-amz-s3session-token: express-session-secret through the connector and asserted that the destination request omitted it. The second added headers.x-api-key=gateway-api-secret to the existing end-to-end storage-option test, asserted it reached the gateway, and asserted it did not reach the destination.

With isolated Cargo/Rustup directories, I ran:

RUSTUP_HOME="$PWD/../rustup" CARGO_HOME="$PWD/../cargo-home" CARGO_TARGET_DIR="$PWD/../target" RUSTUP_TOOLCHAIN=1.97.0 cargo test -p lance-io --features metrics --lib object_store::providers::aws::tests::test_s3_express_session_token_is_not_forwarded -- --exact --nocapture
RUSTUP_HOME="$PWD/../rustup" CARGO_HOME="$PWD/../cargo-home" CARGO_TARGET_DIR="$PWD/../target" RUSTUP_TOOLCHAIN=1.97.0 cargo test -p lance-io --features metrics --lib object_store::providers::aws::tests::test_storage_option_follows_credential_safe_range_redirect -- --exact --nocapture

Expected: both secrets are absent at the destination. Observed: each test failed because its secret was present in the destination request.

@anonx3247
anonx3247 force-pushed the fix/s3-redirect-host branch from e2c32f1 to 7ea1a1b Compare August 5, 2026 18:39
@Xuanwo

Xuanwo commented Aug 5, 2026

Copy link
Copy Markdown
Member

add an opt-in S3 connector for credential-safe cross-authority Range GET redirects

Hi, can you explain first what you want to do here? A misconfigured s3 service is out of the lance security boundary.

I'm going to close this PR first. Feel free to raise a discussion on this.

@Xuanwo Xuanwo closed this Aug 5, 2026
@anonx3247

anonx3247 commented Aug 5, 2026

Copy link
Copy Markdown
Author

Hi @Xuanwo, sorry my agent took some liberties in working and talking for me here, our usecase here is not for accessing an S3 bucket directly, we basically have a proxy service that acts as an S3 endpoint before the underlying S3 bucket, this proxy ensures things like auth and other elements base S3 doesn't impose. We then still return the original S3 endpoint with a redirect to ensure the actual S3 read/write operations aren't slowed or overloading the proxy and to connect directly to the 'real' S3. This is why our redirect changes hosts.

@anonx3247

Copy link
Copy Markdown
Author

Opened the requested ownership/design discussion: #8301

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

Labels

A-deps Dependency updates A-encoding Encoding, IO, file reader/writer A-format On-disk format: protos and format spec docs A-index Vector index, linalg, tokenizer A-java Java bindings + JNI A-python Python bindings bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants