Skip to content

fix: URL-encode uploadId in canonical resource for GCS V2 presigned part URLs#217

Merged
matthiasL-scality merged 2 commits into
mainfrom
fix/presign-part-gcs-signature-v2
Jul 16, 2026
Merged

fix: URL-encode uploadId in canonical resource for GCS V2 presigned part URLs#217
matthiasL-scality merged 2 commits into
mainfrom
fix/presign-part-gcs-signature-v2

Conversation

@matthiasL-scality

Copy link
Copy Markdown
Contributor

Summary

  • GCS V2 signing requires the canonical resource to include URL-encoded values as they appear in the URL (unlike AWS V2 which uses raw/decoded values)
  • Without this fix, uploadIds containing characters like +, / or = (common in GCS base64-encoded IDs) produce a SignatureDoesNotMatch (403) when GCS verifies the presigned part PUT
  • Fix: use ngx.escape_uri(upload_id) in the canonical resource for PRESIGN_PART mode, consistent with the value already used in the presigned URL itself

Reference: GCS V2 Signing Process"copy the HTTP request path literally: that is, you should include all URL encoding (percent signs) in the string that you create."

Root cause

Observed in production on scality/scalityos CI — multipart uploads of large ISO files (50 parts) failed with:

SignatureDoesNotMatch: The request signature we calculated does not match
the signature you provided. Check your Google secret key and signing method.

Test plan

  • test_presign_upload.py — all 7 tests pass (including test_presign_multipart_full_round_trip)
  • test_multipart_upload.py — all 5 tests pass
  • Validate in production by re-running a failing scalityos CI job

🤖 Generated with Claude Code

…art URLs

GCS V2 signing requires the canonical resource to include URL-encoded values
as they appear in the URL (unlike AWS V2 which uses raw/decoded values).
Without this, uploadIds containing characters like +, / or = produce a
signature mismatch (403 SignatureDoesNotMatch) when GCS verifies the request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@matthiasL-scality matthiasL-scality requested a review from a team as a code owner July 16, 2026 08:06
Comment thread lua/compute_aws_s3_signature.lua Outdated
local aws_secret_key = os.getenv('AWS_SECRET_ACCESS_KEY')
-- GCS V2 requires URL-encoded values in the canonical resource (unlike AWS V2 which uses raw values).
-- See: https://cloud.google.com/storage/docs/access-control/signed-urls-v2
local escaped_upload_id = ngx.escape_uri(upload_id)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Potential double-encoding risk: ngx.var.arg_uploadId returns the raw query-string value without percent-decoding (standard nginx $arg_* behavior). If the client percent-encodes the upload ID (e.g. Python requests with params= turns + into %2B), then ngx.escape_uri() double-encodes it (%2B%252B). The signature will be internally consistent (canonical resource matches URL), but GCS will URL-decode the presigned URL once and get %2B instead of +, failing to match the actual multipart upload.

Normalizing first avoids this regardless of how the client encodes:

```suggestion
local escaped_upload_id = ngx.escape_uri(ngx.unescape_uri(upload_id))

@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown
  • Potential double-encoding of uploadId: ngx.var.arg_uploadId returns the raw query-string value (not percent-decoded). Calling ngx.escape_uri() on an already-encoded value produces double-encoding (%2B%252B). The fix ensures signature consistency, but GCS may fail to match the double-encoded upload ID to the actual multipart upload. Use ngx.escape_uri(ngx.unescape_uri(upload_id)) to normalize first.
    • The existing tests likely pass because the test S3 backend (MinIO / fake-gcs-server) generates upload IDs without special characters, so the encoding path is not exercised.
    • Consider adding a test with a synthetic upload ID containing +, /, or = to validate the encoding.

Review by Claude Code

ngx.var.arg_* may return a raw (already percent-encoded) or decoded value
depending on the nginx version. Without normalization, calling ngx.escape_uri()
on an already-encoded value produces double-encoding (%2B → %252B), which GCS
would decode one level and fail to match the multipart upload ID.

Use ngx.escape_uri(ngx.unescape_uri(upload_id)) to normalize to a consistent
single-encoded form regardless of how nginx provides the arg value.

Also adds a test that directly injects a base64-style uploadId (containing +, /
and =) to verify the percent-encoding in the returned presigned URL, covering
the GCS ID format that Cloudserver does not naturally produce.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude

claude Bot commented Jul 16, 2026

Copy link
Copy Markdown

LGTM

Review by Claude Code

@matthiasL-scality matthiasL-scality merged commit 6805a8b into main Jul 16, 2026
6 checks passed
@matthiasL-scality matthiasL-scality deleted the fix/presign-part-gcs-signature-v2 branch July 16, 2026 14:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant