Skip to content

Conversation

@zubron
Copy link
Contributor

@zubron zubron commented Nov 24, 2025

Closes #9599

Change Description

Bug Fix

This change adds validation for the X-Amz-Expires parameter in presigned URLs.

Changes:

  • Parse X-Amz-Expires and validate within allowed range
  • Check expiration time for presigned URLs and deny if request is past expiry time.
  • Add an explicit check for all required parameters for presigned URLs

Testing Details

Unit tests for the verification logic were added.

Also tested using the python test in this gist

Breaking Change?

This could be considered a breaking change if users were unaware of this issue and have been relying on presigned URLs not expiring. However, this is a security issue and should be fixed.

@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch from 01442fe to 3915765 Compare November 24, 2025 15:16
@zubron zubron added the include-changelog PR description should be included in next release changelog label Nov 24, 2025
@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch 3 times, most recently from 77a8138 to f5fb7ba Compare November 27, 2025 15:00
This change adds validation for the `X-Amz-Expires` parameter in
presigned URLs.

Changes:
- Parse `X-Amz-Expires` and validate within allowed range
- Check expiration time for presigned URLs and deny request if past
  expiration time.
- Add an explicit check for all required parameters for presigned URLs

Fixes #9599
@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch from f5fb7ba to 375ec38 Compare November 27, 2025 22:10
This change ensures that when a request doesn't have the v4 algorithm
URL parameter (`x-amz-algorithm`), the chained authenticator tries the
next auth method instead of failing. Previously, requests using other
signature versions would fail because v4 parsing returned and error that
blocked the chain.
@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch from 375ec38 to 07ff93a Compare November 27, 2025 22:14
@zubron zubron marked this pull request as ready for review November 27, 2025 23:21
@zubron zubron requested a review from a team November 27, 2025 23:23
Copy link
Member

@N-o-Z N-o-Z left a comment

Choose a reason for hiding this comment

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

Looks good!

Added comment inline.
In addition:

  1. I think it will be nice to have an esti test that checks the e2e flow with an expired presigned URL request (I think this can be done with a very short expiry) but if you think it's too much work for too less of a value let me know
  2. These changes fix the behavior for the V4 signer but we also have the V2 which although deprecated is still being used in some cases and is still supported in our code. We might want to consult with product regarding that. If we do need to fix this for V2 let's open a separate issue for that so this PR can converge.

if err == nil {
c.chosen = method
return sigContext, nil
} else if !errors.Is(err, ErrHeaderMalformed) && !errors.Is(err, ErrBadAuthorizationFormat) {
Copy link
Member

Choose a reason for hiding this comment

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

Can you please add a comment here explaining why we are singling out these error types

return expires, nil
}

func isV4PresignedRequest(query url.Values) error {
Copy link
Member

Choose a reason for hiding this comment

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

Nice taking this out to a separate method

return ctx, nil
}

// otherwise, see if we have all the required query parameters
Copy link
Member

Choose a reason for hiding this comment

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

I think it's important to keep the comment.
I'd modify it to: "Otherwise try to parse request as presigned URL


func (ctx *verificationCtx) verifyExpiration() error {
if !ctx.AuthValue.IsPresigned {
// TODO: we currently don't have handling for this
Copy link
Member

Choose a reason for hiding this comment

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

Why is that a TODO?
Is there an expiry mechanism for regular requests?

timeDiff := now.Sub(requestTime)

// Check for requests from the future and allow small clock skew
if timeDiff < 0 && timeDiff.Abs() > 5*time.Minute {
Copy link
Member

Choose a reason for hiding this comment

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

Why are we checking for 5min?
What's the behavior in AWS for presigned requests that are less than 5 min in the future?
Please document

"github.com/treeverse/lakefs/pkg/gateway/errors"
)

func TestVerifyExpiration(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

Nice tests - do we want to also add tests for non-presigned with expiry and check our behavior is consistent with S3?

}
}

func TestParseExpires(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

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

We should test for nil expires as well (no header)

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

Labels

include-changelog PR description should be included in next release changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: S3 Presigned GET ignores expiration, can be accessed beyond expiration date

2 participants