Skip to content

fix(native): enforce Secure/Path/Expires cookie attributes in the Swift and Dart cookie jars - #976

Merged
soberm merged 6 commits into
mainfrom
fix/native-cookie-attributes-swift-dart
Oct 24, 2026
Merged

soberm merged 6 commits into
mainfrom
fix/native-cookie-attributes-swift-dart

Conversation

@soberm

@soberm soberm commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The Swift and Dart cookie jars keep only a cookie's name and value: the Secure, Path and Expires/Max-Age attributes are dropped, so every stored cookie for a host is sent on every request to it regardless of scheme or path, and expiry is honored only for a literal max-age=0 (Swift) or not at all (Dart, which also keys cookies by name only). This change makes both jars parse and enforce those attributes when storing and sending cookies, matching what the Kotlin cookie jar already does (#646).

Issue #, if available: N/A

Changes

Rules applied on both platforms (same as Kotlin's PersistentCookiesStorage / Ktor Cookie.matches):

  • A cookie is sent only to the host that set it (host-only; the Domain attribute is not used), only to request paths under its Path (trailing-slash prefix match), and a Secure cookie only over https/wss.
  • A cookie without a usable Path applies to the directory of the request path that set it (/ fallback).
  • Max-Age takes precedence over Expires. Max-Age <= 0 or a past Expires removes the cookie; a stored cookie stops being sent once its expiry arrives.
  • A new cookie replaces the stored cookies of the same name that would be sent on that request.

Swift (native/swift)

  • New StoredCookie (parse, match, expiry, stored record) and CookieJar (store Set-Cookie, build Cookie header) over a CookieItemStore protocol, which KeychainCookieStore now conforms to. BlocksClient.execute and postRawRoute use the jar with the actual request URL.
  • Storage: each Keychain item is keyed host|path|name and holds a JSON record with the attributes. Items written by earlier versions (host|name → bare value) are still read as before (Path=/, not Secure, no expiry), so an existing session keeps working after upgrade; the old item is removed the next time the server sets that cookie. Items written by this version are not readable by earlier versions.

Dart (native/dart/packages/blocks_runtime)

  • Cookies are keyed by host + path + name instead of by name alone.
  • SessionStore gains cookieHeaderFor(Uri) and an optional requestUri on setCookies; BlocksClient and OidcClient pass the request URL to both. cookieHeader and cookies are unchanged in shape (cookieHeader returns every unexpired cookie). Custom SessionStore implementations need to add the new members.
  • Expires is parsed without dart:io so the package stays web-compatible. The Set-Cookie splitter no longer splits at the comma inside an Expires date.
  • Storage: PersistentSessionStore writes a JSON list of cookie records. The name → value map written by earlier versions is still read (Path=/, no Secure, no expiry, any host) and replaced when the server sets the cookie again; an unreadable payload is cleared as before.

Release notes

  • Swift: changeset native/swift/.changeset/swift-cookie-attributes.md, minor (stored record format and cookie send behavior change).
  • Dart: entry under ## Unreleased in blocks_runtime/CHANGELOG.md (the Dart packages don't use changesets), marked breaking for the SessionStore interface additions.

Validation

  • Dart (Dart 3.11.6, Linux): dart format --set-exit-if-changed, dart analyze (no new errors or warnings) and dart test in blocks_runtime — 96 tests pass, including new tests for Secure gating, Path scoping and default path, host scoping, Max-Age/Expires expiry and precedence, joined headers with Expires, host/path keying, persistence round-trip of attributes, and reading/replacing the earlier storage format. blocks_codegen tests also pass. dart test -p chrome was not run locally.
  • Swift: swiftformat --lint and swiftlint lint over the package report no issues. StoredCookie.swift, CookieJar.swift and the new cookie tests in BlocksClientTests were compiled and run standalone with Swift 6.2.4 on Linux (16 tests pass). The full package (swift build/swift test/xcodebuild on the iOS simulator) depends on Apple frameworks and was not built locally — relying on CI for that.

Checklist

  • PR description included
  • Tests are changed or added
  • Relevant documentation is changed or added (and PR referenced)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

…nd Dart

The Swift and Dart cookie jars now keep each cookie's Path, Secure flag
and expiry when storing a Set-Cookie response, and apply them when
attaching cookies to a request, following the Kotlin runtime: a cookie
is sent only to the host that set it, only to request paths under its
Path, and Secure cookies only over https/wss. Max-Age takes precedence
over Expires, and Max-Age <= 0 or a past Expires removes the cookie.
A new cookie replaces the same-named cookies that would be sent on that
request.

Swift moves the jar logic into StoredCookie and CookieJar over a
CookieItemStore protocol; Keychain items hold a JSON record keyed by
host|path|name, and items written under the earlier host|name layout
are still read. Dart keys cookies by host, path and name, adds
SessionStore.cookieHeaderFor and a requestUri argument to setCookies,
and persists a list of records while still reading the earlier
name -> value map.
@changeset-bot

changeset-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: bad8fc5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@soberm
soberm marked this pull request as ready for review October 23, 2026 12:11
@soberm
soberm requested a review from a team as a code owner October 23, 2026 12:11
…icity

Swift and Dart cookie path matching now sends a cookie only when the
request path equals its Path or continues past it at a "/", so a cookie
for /admin/ is no longer attached to /admin. Same-named cookies are listed
longest Path first in the Cookie header. The Swift CookieJar runs its
read and replace steps under one lock so concurrent responses apply in
sequence. The PersistentSessionStore docstring now describes the JSON
list format it writes.
cadivus
cadivus previously approved these changes Oct 23, 2026

@cadivus cadivus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved but I am not sure if the two comments. Eventually look into that.

… them

After a followed redirect, Set-Cookie headers belong to the last URL in
the chain. Swift now uses HTTPURLResponse.url, and Dart sends cookie-
carrying POSTs through a helper that reads BaseResponseWithUrl.url,
falling back to the requested URL.
Treat a Set-Cookie with an empty value as a removal in the Dart cookie
jar, matching the Swift jar. Document and test that cookies read from
the earlier name-to-value format carry no host until the server sets
them again.
@soberm

soberm commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Both items from the review are addressed in 07fdef7.

Empty-value cookies (session_store.dart, setCookies) — Fixed by aligning Dart with Swift. The Swift jar's save (CookieJar.swift:68) first replaces the same-named cookies that match the request, then removes rather than stores a cookie whose value is empty. Dart now does the same at the same point: setCookies still replaces the matching same-named cookies, and only stores the parsed cookie when parsed.value.isNotEmpty && !parsed.isExpired(_now) (session_store.dart:156). As on Swift, nothing changes when sending, since an empty-value cookie is never stored. The SessionStore.setCookies docstring (session_store.dart:16) now states that an empty value is treated as a removal to match the Swift jar, and that this is a convention of these jars rather than an RFC 6265 deletion rule. Tests added in persistent_session_store_test.dart: an empty value deletes a stored cookie (a=1 then a=; Path=/, nothing stored or sent) and an empty value is not stored (a=; Path=/ alone). The CHANGELOG entry now notes the rule.

Host scoping of cookies read from the earlier format (session_store.dart, load) — Documented as a transitional difference and covered by a test, without changing the null-host matching. Swift can recover the host from its old host|name key; the Dart name → value map never recorded a host, so there is nothing to recover. Scoping on first read would have to guess the host from whichever request comes first, which adds a behavior of its own for a one-time upgrade window. What changed:

  • Comments at the legacy branch in load (session_store.dart:360) and on _StoredCookie.host (session_store.dart:65), plus the load docstring (session_store.dart:344), explain that these cookies have no host because the old format stored none, so they match any host until the server sets them again.
  • load adopts cookies persisted as a name → value map now also checks that the adopted cookie is sent to another host before it is set again, and that the replacement the server sets is scoped to its host and no longer sent there.
  • The CHANGELOG ## Unreleased entry states the difference from Swift and that it covers only cookies written before the upgrade, until the server sets each one again.

Verification (Dart 3.11.6, native/dart/packages/blocks_runtime): dart format --set-exit-if-changed clean; dart analyze 87 issues, the same as before the change, 0 errors; dart test 102 passed. Swift was not changed.

…ributes-swift-dart

# Conflicts:
#	native/swift/Sources/BlocksRuntime/KeychainCookieStore.swift
@soberm
soberm merged commit d574752 into main Oct 24, 2026
54 of 55 checks passed
@soberm
soberm deleted the fix/native-cookie-attributes-swift-dart branch October 24, 2026 06:13

This branch was previously deployed

1 inactive deployment
e2e — bad8fc53 Deployed Oct 23, 2026 by soberm via Swift E2E (sandbox) #3986
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.

2 participants