Skip to content

test(storage): pin S1-S8 contracts without flipping defaults - #3416

Merged
bpamiri merged 2 commits into
developfrom
cursor/storage-hardener-s1-s8-bf67
Aug 25, 2026
Merged

test(storage): pin S1-S8 contracts without flipping defaults#3416
bpamiri merged 2 commits into
developfrom
cursor/storage-hardener-s1-s8-bf67

Conversation

@bpamiri

@bpamiri bpamiri commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Why

LocalDisk $resolve, LocalDisk verifySignature, S3Disk delete(), and S3Signer signedHeaders() 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 — optional range on signedHeaders(). Empty range keeps today's canonical request.
  • vendor/wheels/storage/drivers/LocalDisk.cfc$resolve is public. Find("..") and concat are unchanged.
  • vendor/wheels/storage/drivers/S3Disk.cfc$request is public. delete() still returns true after $assertSuccess.
  • vendor/wheels/tests/specs/storage/StorageSpec.cfc — S1, S2, S4, S7, S8 it() blocks.
  • vendor/wheels/tests/_assets/storage/S3DiskDeleteStub.cfc — 204 stub for the S4 contract.
  • changelog.d/storage-hardener-s1-s8.changed.md

Out: empty or slash-key throw, url() / signedUrl() key encoding, HEAD-then-false delete, S3 ACL headers, expiresIn clamp or throw. Policy leftovers stay closed.

Tradeoffs

The official AWS GET Object header-auth vector signs Range: bytes=0-9. signedHeaders() takes an optional range so 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. S3 delete() still returns true after any 2xx, including a missing key. Apps that already call $resolve or $request see the same logic with public visibility.

Desk S1–S8

ID Status One line
S1 PROVEN (current root only) $resolve("", "/", "///") is root/, root//, root////. get() throws NotFound, not InvalidKey.
S1 throw HELD Empty or slash-only keys still do not throw.
S2 PROVEN Symlink-under-root fixture. exists("link/secret.txt") is not InvalidKey. get() reads the outside canary. Resolver unchanged.
S3 encode HELD url() / signedUrl() key encoding unflipped.
S4 PROVEN Stubbed 204. delete("gone.txt") is true twice. No HEAD-then-false.
S4 HEAD-then-false HELD Missing objects still return true after 2xx.
S5 HELD S3 ACL unflipped.
S6 HELD expiresIn clamp or throw unflipped.
S7 PROVEN verifySignature with no signingKey returns false. signedUrl throw is the existing spec.
S8 PROVEN Official AWS GET Object header-auth vector. Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41.

Verification

Command actually run:

wheels test --core --ci --filter=storage

Scope: wheels.tests.specs.storage

bundlesDiscovered=1
32 passed, 0 failed, 0 error, 0 skipped
Lucee 7.0.0.395 / sqlite
directoryRejected=false

The 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

8191659e500e093c49336cb9a16e351235317334

Base tip: 3bb4bcd3be7592e8fc04fc5c90964c7d72e2091f

No closer keywords. Do not merge.

Open in Web Open in Cursor 

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
bpamiri marked this pull request as ready for review August 25, 2026 03:19
@bpamiri
bpamiri merged commit 80d3e0f into develop Aug 25, 2026
12 checks passed
@bpamiri
bpamiri deleted the cursor/storage-hardener-s1-s8-bf67 branch 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants