storage: V4 getSignedUrl sorts canonical headers with localeCompare, so they disagree with X-Goog-SignedHeaders

Open Beginner friendly
#9,840 1 comment 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@itsrayeedscodespace is already working on this.

Since Nov 8, 2026.

  • #9841 by @itsrayeedscodespace — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
node.js, typescript
Domain
backend, security

Research direction

Start in handwritten/storage/src/signer.ts, at URLSigner.getCanonicalHeaders, and compare its header ordering with the plain .sort() used for signedHeaders. Check the storage signer tests for coverage of header names containing - and _ and for locale-sensitive ordering. Done when the canonical headers block follows code point order and matches the signed headers list.

Written by the indexing model from the issue text.

Description

api: storage
Please make sure you have searched for information in the following guides.
Library Name

@google-cloud/storage

A screenshot that you have tested with "Try this API".

Not applicable: V4 URL signing happens entirely in the client library; no API call is involved.

Link to the code that reproduces this issue. A link to a public Github Repository or gist with a minimal reproduction.

https://gist.github.com/trackoor/dc977f474cf9a692628c158959bbef2c

A step-by-step description of how to reproduce the issue, based on the linked reproduction.
  1. npm install (installs @google-cloud/storage 8.4.0), Node.js v26.13.0 (full ICU), Linux.
  2. node index.js prints the canonical request that gets signed, for extensionHeaders: {"x-goog-meta-a_b": "1", "x-goog-meta-a-b": "2"}:
GET
/test-bucket/test-object
X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=test%40example.iam.gserviceaccount.com%2F20261107%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20261107T105715Z&X-Goog-Expires=60&X-Goog-SignedHeaders=host%3Bx-goog-meta-a-b%3Bx-goog-meta-a_b
host:storage.googleapis.com
x-goog-meta-a_b:1
x-goog-meta-a-b:2

host;x-goog-meta-a-b;x-goog-meta-a_b
UNSIGNED-PAYLOAD
  1. LANG=lt_LT.UTF-8 node index.js lt does the same for x-goog-meta-y and x-goog-meta-j:
host:storage.googleapis.com
x-goog-meta-y:1
x-goog-meta-j:2

host;x-goog-meta-j;x-goog-meta-y
A clear and concise description of what the bug is, and what you expected to happen.

The canonical headers block is in a different order from the signed headers list in the same canonical request, so GCS computes a different signature and rejects the URL. Expected: canonical headers in code point order, matching X-Goog-SignedHeaders:

host:storage.googleapis.com
x-goog-meta-a-b:2
x-goog-meta-a_b:1

host;x-goog-meta-a-b;x-goog-meta-a_b
A clear and concise description WHY you expect this behavior, i.e., was it a recent change, there is documentation that points to this behavior, etc. **

The V4 canonical request docs say headers are sorted by name "using a lexicographical sort by code point value": https://cloud.google.com/storage/docs/authentication/canonical-requests

URLSigner.getCanonicalHeaders in handwritten/storage/src/signer.ts (line 401 on main at 97b0d64daa8ea9407756aee646b8ac1bf19aec80) sorts with a[0].localeCompare(b[0]), which puts _ before - and follows the process locale, while signedHeaders uses a plain .sort().

Credit: https://ucsbplse.github.io/

Dominant language
TypeScript
Stars
3.2k
Forks
732
Avg merge
3d 7h
Merged PRs (30d)
117

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from googleapis/google-cloud-node

All issues in googleapis/google-cloud-node

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.