test(storage): pin S1-S8 contracts without flipping defaults - #3416
Merged
Conversation
LocalDisk $resolve keeps empty and slash-only keys as root paths.
The symlink-under-root fixture shows Find("..") misses that hop.
verifySignature returns false when signingKey is empty.
S3Disk.delete() stays true after 2xx. signedHeaders pins the official
AWS GET Object header-auth signature via an optional range argument.
Signed-off-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Peter Amiri <peter@alurium.com>
The range branch, AWS vector call, and symlink fixture already state the contract. The comments restated it. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Peter Amiri <peter@alurium.com>
bpamiri
marked this pull request as ready for review
August 25, 2026 03:19
bpamiri
added a commit
that referenced
this pull request
Aug 25, 2026
* test(storage): flip S1 S3 S4 S5 S6 specs off the #3416 pins Empty and slash-only keys must throw InvalidKey. LocalDisk url() and signedUrl() must encode spaces and reserved characters. S3Disk.delete() must HEAD then return false when missing. put() must send a signed x-amz-acl. expiresIn outside 1..604800 must throw on both disks. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Peter Amiri <peter@alurium.com> * fix(storage): throw empty keys, encode local URLs, and bound expiry LocalDisk $resolve throws InvalidKey for empty and slash-only keys. url() and signedUrl() encode like S3. S3Disk.delete() HEADs then returns false on a miss. put() sends a signed x-amz-acl. expiresIn outside 1..604800 throws InvalidExpiresIn on both disks. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Peter Amiri <peter@alurium.com> * style(storage): drop leftover header-list and delete comments The signedHeaders header list is now built from range and acl. \$assertSuccess already fails a non-2xx DELETE. Signed-off-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Peter Amiri <peter@alurium.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
LocalDisk
$resolve, LocalDiskverifySignature, S3Diskdelete(), and S3SignersignedHeaders()already had these contracts. The spec did not pin them. A later canonicalize, HEAD-then-false delete, or unsigned header-auth rewrite would merge without a failing test.This PR proves S1 (current root behavior), S2, S4 (idempotent-true), S7, and S8. Holds stay unflipped. No public default changes.
Scope
In:
vendor/wheels/storage/S3Signer.cfc— optionalrangeonsignedHeaders(). Empty range keeps today's canonical request.vendor/wheels/storage/drivers/LocalDisk.cfc—$resolveis public.Find("..")and concat are unchanged.vendor/wheels/storage/drivers/S3Disk.cfc—$requestis public.delete()still returns true after$assertSuccess.vendor/wheels/tests/specs/storage/StorageSpec.cfc— S1, S2, S4, S7, S8it()blocks.vendor/wheels/tests/_assets/storage/S3DiskDeleteStub.cfc— 204 stub for the S4 contract.changelog.d/storage-hardener-s1-s8.changed.mdOut: empty or slash-key throw,
url()/signedUrl()key encoding, HEAD-then-false delete, S3 ACL headers,expiresInclamp or throw. Policy leftovers stay closed.Tradeoffs
The official AWS GET Object header-auth vector signs
Range: bytes=0-9.signedHeaders()takes an optionalrangeso the published signature can be pinned. Callers that omit it keep the current three-header set.S2 is a fixture, not a canonicalize. The spec shows that
Find("..")does not reject a symlink hop under root. The resolver is unchanged. Not an ESCALATE.Blast Radius
Existing
signedHeaders(method, key, payload)callers are unchanged. LocalDisk still rejects".."by substring and concatenates onto root. S3delete()still returns true after any 2xx, including a missing key. Apps that already call$resolveor$requestsee the same logic with public visibility.Desk S1–S8
$resolve("", "/", "///")isroot/,root//,root////.get()throwsNotFound, notInvalidKey.exists("link/secret.txt")is notInvalidKey.get()reads the outside canary. Resolver unchanged.url()/signedUrl()key encoding unflipped.delete("gone.txt")is true twice. No HEAD-then-false.expiresInclamp or throw unflipped.verifySignaturewith nosigningKeyreturns false.signedUrlthrow is the existing spec.Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41.Verification
Command actually run:
Scope:
wheels.tests.specs.storageThe same filter after moving the delete stub out of
specs/storage/stayed at 32 passed and dropped the empty extra bundle. The comment-only follow-up stayed at 32 passed.Adobe and BoxLang were not run here.
HEAD
8191659e500e093c49336cb9a16e351235317334Base tip:
3bb4bcd3be7592e8fc04fc5c90964c7d72e2091fNo closer keywords. Do not merge.