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 2 times, most recently 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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added a comment here, hopefully it explains it clearly!

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment replaced and updated


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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a misunderstanding here of how AWS handles clock skew for signed requests. After testing against AWS S3: AWS validates clock skew for presigned URLs when X-Amz-Date is in the future. If that time is >15 minutes ahead of the server time, the requests are rejected and anything within 15 minutes is tolerated. No clock skew check for past timestamps (only expiration). I'll fix this and add comments.

"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?

Copy link
Contributor Author

@zubron zubron Dec 2, 2025

Choose a reason for hiding this comment

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

I'm going to add those tests in a follow up.

}
}

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)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parseExpires is only called after isV4PresignedRequest validates that X-Amz-Expires exists in the request. At that point, we have a string value which can't be nil. The empty string case is already covered in this test and the tests for isV4PresignedRequest check for the missing expiry param. Did you have a different test in mind?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parseExpires is only called after isV4PresignedRequest validates that X-Amz-Expires exists in the request. At that point, query.Get() returns a string (which can't be nil). The empty string case is already covered by the existing test and the tests for isV4PresignedRequest check for the missing expiry param. Did you have a different test in mind?

@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch from 07ff93a to d5ee853 Compare December 1, 2025 14:25
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.
Updates future timestamp validation to match AWS S3 behaviour. AWS
tolerates future clock skew of up to 15 minutes for presigned requests.
@zubron zubron force-pushed the fix/presigned-urls-ignore-expiration-9599 branch from d5ee853 to 99a1f85 Compare December 1, 2025 15:49
@zubron
Copy link
Contributor Author

zubron commented Dec 2, 2025

Thanks for the review @N-o-Z! I think I've addressed your comments, and I also added some esti tests to cover the different expiry situations. I agree that anything related to V2 can be addressed in a follow up 👍

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.

Thank you for addressing the comments and adding the tests!

return errors.ErrRequestNotReadyYet
}

// Calculate expiration from the signed time, not current time
Copy link
Member

Choose a reason for hiding this comment

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

👍🏽

@N-o-Z
Copy link
Member

N-o-Z commented Dec 2, 2025

Thank you for addressing the comments and adding the tests!

Please link follow-up issue to this task

@zubron zubron merged commit 92966ae into master Dec 2, 2025
144 of 148 checks passed
@zubron zubron deleted the fix/presigned-urls-ignore-expiration-9599 branch December 2, 2025 17:39
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