Repository navigation
4.0.0 release prep - #17
Merged
Merged
Conversation
…MongoDB 4.0.3 Replace the branch: "main" sibling dependencies with from: the released versions, and raise Perfect-MongoDB from 4.0.2 to 4.0.3. swift package resolve picks CRUD 3.0.1, MySQL 4.0.0, PostgreSQL 5.0.0, Redis 4.0.0, SQLite 6.0.0 and MongoDB 4.0.3. Package.resolved stays untracked (.gitignore). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each backend's live suite now uses .enabled(if:) instead of returning early, so a disabled suite reports as skipped rather than as passing tests. Each has a serverIsReachable test, so an unreachable server fails the run, and a top-level test fails when SESSION_REQUIRE_LIVE=1 but the backend's switch isn't set (CI sets it). MySQL and PostgreSQL read MYSQL_PORT and PG_PORT; MySQL's live tests require MYSQL_PORT so they never fall back to a local server on 3306. MongoDB's two SessionDocument tests, which need no server, move to their own suite so they always run. Verified: unset REDIS_TESTS with SESSION_REQUIRE_LIVE=1 fails; pointing each backend at a closed port fails its serverIsReachable test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PerfectSession.tojson() and the Redis driver's encoder called
JSONSerialization.data(withJSONObject:) under try?, but on Darwin it raises
an Objective-C NSInvalidArgumentException for a Date, URL, NaN, infinity or
a non-string dictionary key, which Swift can't catch: storing a Date in
session.data and saving terminated the server. Check isValidJSONObject
first, so tojson() returns "{}" as its fallback already intended, and the
Redis driver saves empty data. Document which values data can hold.
Reproduced: the new core test and the Redis live test both aborted with
signal 6 (NSInvalidArgumentException) before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CORSSecurity.isAcceptable accepted an origin when it started with the text before a pattern's * OR ended with the text after it, so the pattern "https://*.example.com" accepted every https origin, and "http://localhost:*" every http origin. Match the whole pattern instead (each * matches any run of characters), and compare patterns ignoring case as origins already were. Reproduced: the new test failed with 5 issues before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three fixes in RedisSessionDriver, adapted to Perfect-Redis 4.0.0: - Keys were the bare token, and tokens come from clients' cookies. A cookie naming one of the application's own keys resumed it as a session (a JSON value with a "userid" became that user's session), and logging out with it deleted the key. Keys are now RedisSessionConnector.keyPrefix + token (default "perfect-session:"). Sessions stored before this aren't found. - save() used a plain SET, so a save racing a logout (destroy) stored the session again. It now uses SET ... XX and logs at debug when nothing matched, as the MongoDB driver does. create() uses NX. - The TTL was SessionConfig.idle, not the session's own idle, and an idle of 0 or less stored the key with no TTL at all, so it never expired. The TTL is now session.idle; save() deletes a session whose idle is 0 or less, and create() doesn't store one. Also: RedisSessionConnector.commandTimeout passes through to RedisClientIdentifier (default nil, Perfect-Redis's default); every call closes its connection on the error path too; a failed resume is logged. Reproduced: saveAfterDestroyDoesNotResurrect, expiryFollowsSessionIdle, nonPositiveIdleDoesNotMakeASessionImmortal, tokenCannotReachOtherKeys and keysAreNamespaced failed (7 issues) before the change. Tokens with spaces, colons, CR/LF, NUL or RESP framing were already harmless (bulk strings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MemorySessionDriver.save() inserted unconditionally, so a save racing a logout brought the session back, and saving a PerfectSession built around a client-chosen token stored it. It now only updates a stored session, like the SQL drivers' UPDATE, MongoDB's upsert: false and Redis's SET XX. Live tests confirm MySQL, PostgreSQL and SQLite already behaved this way. Reproduced: memorySaveOnlyUpdatesStoredSessions failed with 2 issues before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PerfectSession.isValid(), MemorySessionDriver.clean() and the MongoDB SessionDocument computed updated + idle with a trapping add, so an idle of Int.max (a natural "never expire") crashed the process. Add PerfectSession.expires, the sum clamped instead of overflowing, and use it. The MongoDB driver also clamps expiresAt to years 0001-9999: Perfect-MongoDB 4.0.3's BSONEncoder traps converting a Date beyond Int64 milliseconds to a BSON date (BSONCodable.swift:279), which a large idle reached even without the overflow. Reproduced: hugeIdleDoesNotTrap trapped (signal 5) in both the core and MongoDB test bundles before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setup() created created/updated/idle as 32-bit int (MySQL) and int4 (PostgreSQL). Timestamps stop in 2038, and an idle time past Int32.max (about 68 years, or Int.max for "never") made the INSERT/UPDATE fail, so the session silently wasn't stored or saved. New tables use bigint/int8; the MySQL driver reads either width. clean() compared updated + idle in integer arithmetic, which errors on overflow, so one session with a huge idle made every clean() fail; it now sums as DECIMAL (MySQL) or numeric (PostgreSQL). Tables created by earlier versions keep their 32-bit columns (CREATE TABLE IF NOT EXISTS); the README says how to ALTER them. Reproduced: timesPast32BitsRoundTrip failed on MySQL and PostgreSQL before the schema change (idle came back wrong), and its clean() check failed on both with the old sum. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostgreSQLSessionConnector.connectionString() encoded with .urlQueryAllowed, which leaves /, @, :, ? and = unencoded, so a password (or user name or database) containing one of them produced a URI libpq parsed differently: the driver couldn't connect. Encode everything but RFC 3986's unreserved characters; the host also keeps [ ] : for IPv6 literals. Add connectionString(username:password:host:port:database:) so the encoding can be tested without touching the shared settings. Reproduced: passwordWithReservedCharactersConnects (trust auth, password "p@ss/w:rd?x#1") failed to connect and the string test failed before the fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… timeout resume() returned sessions whose idle time had run out (until clean() or, for MongoDB, the TTL monitor deleted them), leaving the check to callers' PerfectSession.isValid(). Middleware that forgot it kept expired sessions alive. Each driver's resume() now throws InvalidSessionError for an expired session, so idle expiry is enforced on the server side. Add PerfectSession.isExpired(at:absoluteTimeout:) and SessionConfig.absoluteTimeout (seconds from creation; default 0, no limit), checked by isExpired(), isValid() and every resume(). There was no way to cap a session's lifetime while it stayed active. The clean() tests now check the stored rows/documents directly, since resume() no longer shows whether clean() deleted anything. Reproduced: resumeRejectsExpired failed on the memory, MySQL, PostgreSQL, SQLite and MongoDB drivers before the change (Redis already expires keys by TTL). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tokens were UUID().uuidString. On Linux (Swift 6.2 and 6.4), Foundation's UUID() fills its bytes by reading /dev/urandom and ignores a failed open(): in a process that has run out of file descriptors (EMFILE, a real state for a loaded server) every UUID is 00000000-0000-4000-8000-000000000000, so every new session, and its CSRF token, got the same value. Add PerfectSession.newToken(): 32 bytes from SystemRandomNumberGenerator (getrandom(2) on Linux, arc4random_buf(3) on Darwin; it never returns unfilled bytes) as 64 lowercase hex digits. Every driver and setCSRF() use it. Tokens are now 64 characters instead of 36; every backend's token column/key holds them. Reproduced in swift:6.2-noble and swift:6.4-noble containers (ulimit -n 256, then UUID() twice: identical all-zero UUIDs). The new exit test tokensStayRandomWithoutFileDescriptors, which starves a child process of descriptors, failed on Linux 6.4 before the change and passes after; on macOS UUID() was never affected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing let an application move a session to a new token, so a token an attacker planted or saw before sign-in stayed valid after it. Add regenerate(_:) to SessionDriver, with a default implementation for every driver: create a session (new token, new CSRF token), copy the user, data and idle time into it, save it, and destroy the old one. Applications call it on sign-in and privilege changes and send the client the new token. Client-supplied tokens for sessions that don't exist were already refused by every driver's resume(); save() no longer stores them either (the memory-driver fix), so a planted token can't be adopted. Tests: memoryRegenerate and a live regenerateMovesSessionToNewToken for each backend (MySQL, PostgreSQL, SQLite, Redis, MongoDB). There was no API to test before this commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ings SessionConfig had the cookie settings (name, Domain, Path, Secure, HttpOnly, SameSite; HttpOnly and SameSite=Strict by default), but nothing in the package applied them: each middleware built the header by hand. Add SessionCookie.setCookie(token:) and expiredCookie(). They add Secure whenever SameSite=None or the name has a __Secure-/__Host- prefix, and return nil instead of a header a browser would misread when the name, token, path or domain contains ; , CR, LF, whitespace or quotes, or a __Host- cookie has a Domain or a path other than /. New feature, so its tests (SessionCookieTests) are new rather than a before/after reproduction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verification only; no code change. SQL text in tokens, user IDs and data
keys/values ('...' OR, backtick and double-quote DROP TABLE, %, backslash)
is bound as parameters on MySQL, PostgreSQL and SQLite and round-trips as
text. On MongoDB, $-prefixed and dotted keys at any depth round-trip as
data, and operator-like tokens match nothing (the _id filter is a typed
string).
Not covered: a NUL in a user ID or user agent. PostgreSQL text can't hold
it (the write fails) and SQLite's text binding stops at it; documented as
a limitation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add concurrentSaveAndResume for every driver (40 concurrent saves and resumes of one session: last writer wins, the CSRF token survives). ThreadSanitizer reported a data race in the core tests: sessionIsValidUALock and sessionIsValidIPLock wrote SessionConfig.userAgentLock/IPAddressLock while tests in parallel called isValid(), which reads them. The CSRF and CORS tests had the same pattern. So the checks can be tested (and called) without the globals, PerfectSession.isValid gains ipAddressLock: and userAgentLock:, CSRFSecurity.isValid acceptableHostnames:, and CORSSecurity.isAcceptable config:, each defaulting to SessionConfig's value; existing calls are unchanged. The tests now pass explicit values. The fd-starvation token test is skipped under ThreadSanitizer, whose runtime aborts a process with no descriptors left. swift test --sanitize=thread (macOS, Xcode 27, all five live backends): three runs, no reports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MySQLSessionDriver, PostgreSQLSessionDriver and SQLiteSessionDriver are final classes with no stored properties, so the compiler checks their Sendable conformance; @unchecked only hid that. Document why PerfectSession (data is [String: Any]) and MemorySessionDriver (NSLock) keep @unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MySQL driver converted MySQLSessionConnector.port with UInt32(_:), which
traps on a negative value. Use UInt32(exactly:) and refuse ports outside
1...65535 (port 0 would silently mean 3306): the connection stays unopened,
so create() stores nothing and resume() throws InvalidSessionError.
Also extend the SQL text probes with non-ASCII (emoji, accented) values,
which round-trip on all three SQL backends.
Reproduced: invalidPortDoesNotTrap crashed the MySQL test process ("Negative
value is not representable") before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uired Bring the workflow up to the wave-3 standard: permissions contents: read, 30-minute timeouts, TZ=America/Denver, and every build with -warnings-as-errors (release with -enable-testing for @testable). Linux: swift:6.2-noble and swift:6.4-noble (was 6.4-resolute only), debug and release, against MongoDB 8, PostgreSQL 17, MySQL 8.4 and Redis 7 services. Ubuntu 24.04's libmongoc is 1.x, so libmongoc 2.5.5 is built from source and cached, as Perfect-MongoDB's CI does. SESSION_REQUIRE_LIVE=1 makes a backend whose tests aren't switched on, or can't reach its server, fail the job instead of skipping. MYSQL_PORT/PG_PORT/REDIS_PORT are explicit. macOS: macos-26 with Xcode 26.0.1 and 26.6, debug and release; no database servers there, so SQLite runs and the other live suites report as skipped. No iOS job: the package declares only macOS (and builds on Linux). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
English README: install with from: "4.0.0"; accurate requirements and CI coverage; a session-security section (tokens, server-side expiry and absoluteTimeout, regenerate() against fixation, SessionCookie, JSON-only data, set-once configuration); a Perfect-NIO 0.1 session-filter example (compiled against Perfect-NIO 0.1.0 with warnings as errors); per-backend notes (Redis keyPrefix and commandTimeout, 64-bit SQL columns); the test variables including the ports and SESSION_REQUIRE_LIVE; known limitations; "Migrating from 3.x" and "From the Swift 6 main branch" (with the ALTER TABLE statements); the legacy branch. README.zh_CN.md gets the same structure in summary and points to the English README for details. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…entication AuthFilter.shouldWeAccept compared raw strings, so behind Perfect-NIO 0.1.0 (whose router matches case-insensitively) /Admin, /ADMIN and /Admin/users reached /admin handlers with no authentication: the review got 200s from a live NIO app. //admin, /./admin, /admin/, /%61dmin and /admin;x=1 weren't matched either, a "*.json" pattern never matched, and "/admin*" covered /administrator. Add AuthFilter.requiresAuthentication(_:using:): - the path is split on /, with empty and . segments dropped; - segments compare ignoring case, unless the new AuthenticationConfig.caseInsensitive (default true) is turned off; - a pattern covers its own path and everything below it, by whole segments (/admin covers /admin/users, not /administrator); - * is a wildcard only as a pattern's whole final segment; an inclusion that uses it any other way requires authentication everywhere, and such an exclusion matches nothing; - when anything is protected, a path with a .. segment, %, ; or \ requires authentication (fail closed). shouldWeAccept, whose name suggested the opposite of what it returns, is deprecated and now calls requiresAuthentication. AuthenticationConfig.denied, which nothing reads, is deprecated. Reproduced: the new tests failed with 18 issues against the old matching (every case and normalization variant, the partial wildcard, /admin/* not covering /admin). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One non-JSON value in session.data (a Date, say) made tojson() return
"{}" and the Redis driver save empty data: every backend but memory saved
the session with ALL its data gone, CSRF token included, while userid
survived, so the user stayed signed in and a CSRF check comparing against
data["csrf"] ?? "" could pass. Add PerfectSession.jsonSafeData(), which
drops only the top-level keys whose values JSON can't hold and logs their
names (never their values) at warning level; a csrf value that isn't JSON
is replaced with a new token rather than dropped. tojson() and the Redis
encoder use it. Every driver's resume() now calls setCSRF(), so a stored
session without a CSRF token gets one.
PerfectSessionCore now depends on swift-log for that warning.
Reproduced: nonJSONValueDropsOnlyThatKey and resumeRestoresMissingCSRF
failed on MySQL, PostgreSQL, SQLite, Redis and MongoDB (3 issues each),
and the core tojson tests failed (15 issues), before the change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the Redis driver called touch() in save(). On the memory, MySQL,
PostgreSQL, SQLite and MongoDB drivers a session expired idle seconds after
it was created however often it was saved, contrary to the README and to
SessionConfig.idle's doc ("each save renews it"), and unlike 3.1.5's memory
driver, which touched on save. Every save() now touches.
Tests that expired a session by back-dating updated before save() now use a
negative idle, since save() sets updated.
Reproduced: saveRenewsIdle (idle 5, saves 3 s apart, resumed 6 s after
creation) failed on memory, MySQL, PostgreSQL, SQLite and MongoDB before
the change. An earlier version of the test with idle 3 was flaky from
whole-second rounding, Redis included; idle 5 passed 18 of 18 runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default regenerate() gave the new session a new created, so every sign-in (or any regenerate) restarted SessionConfig.absoluteTimeout. It now copies created from the old session, and the SQL drivers' save() writes created (MySQL, PostgreSQL and SQLite only updated userid, updated, idle and data, so the copied value would have been lost on those backends). Reproduced: regenerateKeepsCreated failed on all six drivers (memory, MySQL, PostgreSQL, SQLite, Redis, MongoDB) before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Redis, SQLite and MongoDB drivers put the full session token in "eventid" log metadata on every failure (and MongoDB's save on a debug line), so anyone who could read the logs could take over those sessions. Add PerfectSession.logID(_:), the first 12 hex digits of the token's SHA-256 (a small FIPS 180-4 implementation in the core, checked against the standard vectors), and log it as "session" metadata instead. No other log line includes a token. Reproduced: redis/sqlite/mongoDBLogsDoNotContainTokens, exit tests that bootstrap a capturing log handler and drive a driver whose backend is unreachable, failed (token found in the logs) before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
These settings were carried over from 3.x but nothing in 4.0 reads them, so code that sets them (requireToken, failAction...) gets no enforcement and no warning. Each is now deprecated with a message saying it has no effect in 4.0 and what to do instead: SessionConfig.CSRF.failAction, checkState, checkHeaders and requireToken; SessionConfig.CORS.methods, customHeaders, withCredentials and maxAge; SessionConfig.purgeInterval, healthCheckRoute and isOAuth2; PerfectSession._isOAuth2 (no longer initialized from isOAuth2: neither was ever stored); AuthenticationConfig.denied was deprecated in the AuthFilter commit. Add CSRFSecurity.tokenMatches(_:_:), a comparison of the presented and session CSRF tokens whose time doesn't depend on where they differ; an empty token never matches. There was no helper, and the README never said how to check the token (the README commit adds a middleware example). New API, so csrfTokenMatches is a new test rather than a reproduction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An idle near Int.max became a PX argument that Perfect-Redis (Int milliseconds) and Redis (now + PX must fit in 64 bits) refuse, so save() failed, logged, and the session's new data was lost. The TTL is now capped at 10^12 seconds (about 31,700 years); the session's own idle is still stored unchanged. Reproduced: hugeIdleStillSaves (idle Int.max, Int.max / 1000, 2^50) failed with 4 issues before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tim's decision. RedisSessionConnector.commandTimeout was nil, so a Redis server that stopped answering held every request that touched a session indefinitely. It now defaults to 5 seconds; set nil to wait indefinitely, or another value. Behaviour change: a Redis call that takes longer than 5 s now throws and its connection is closed (create/save/destroy log the error; resume throws InvalidSessionError). Test: commandTimeoutDefaultsToFiveSeconds checks the default and resumes against a TCP listener that never answers. Before the change the call was still waiting when the test's 15 s guard fired; now it throws after about 5 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browsers match the cookie name prefixes ignoring case, but SessionCookie only recognized "__Host-" and "__Secure-" exactly: a "__HOST-" name with a Domain got a header browsers reject, and "__host-"/"__SECURE-" names didn't get Secure. Reproduced: sameSiteNoneAndPrefixesForceSecure failed with 3 issues before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
isValid(origin:host:) stripped the scheme from both sides and compared the rest exactly, so an http:// Origin passed for an https:// host, an Origin of https://example.com didn't match a Host of example.com:443, and the Host header's case mattered. It now compares lowercased host[:port]s, treats :443 (https) and :80 (http) like no port, requires the schemes to match when both sides give one (a side without one takes the other's), and never accepts an empty or "null" Origin. acceptableHostnames entries are compared the same way. Reproduced: csrfSecurityComparesSchemePortAndCase failed (7 issues with the updated csrfSecurityStripsScheme, which had asserted the http/https match) before the change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SessionConfig.absoluteTimeout was enforced by isExpired() and resume() but storage ignored it: clean() deleted only sessions past their idle time, the MongoDB TTL index expired documents at updated + idle, and Redis keys lived for idle seconds after each save. Add PerfectSession.expiry(absoluteTimeout:) (the earlier of updated + idle and created + absoluteTimeout) and use it: every driver's clean() also deletes sessions created absoluteTimeout or more seconds ago (overflow-safe on each backend), MongoDB's expiresAt is the expiry, and the Redis TTL runs to it (at least 1 second; save() deletes a session already past it). clean() reads SessionConfig.absoluteTimeout through an internal clean(absoluteTimeout:), which the tests call so they don't change shared settings under other suites. cleanHonoursAbsoluteTimeout (MySQL, PostgreSQL, SQLite), cleanAndTTLHonourAbsoluteTimeout (MongoDB), memoryCleanHonoursAbsoluteTimeout, ttlHonoursAbsoluteTimeout (Redis) and expiryTakesTheEarlierLimit are new: before this commit the old clean() deleted nothing for an active session past the absolute timeout, but there was no parameter to test it through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Session security: every save renews the idle time; absoluteTimeout also governs clean(), the MongoDB TTL and Redis TTLs; regenerate keeps the creation time; a CSRF section (tokenMatches, the Origin check); the AuthFilter rules (segments, case-folding, wildcards, fail-closed characters); CORS pattern advice (no trailing *, "*" accepts null, credentials are the app's job, also in CORSSecurity's doc comment); non-JSON values drop only their own key; logs carry logID, not tokens. - A Perfect-NIO 0.1 middleware example running requiresAuthentication, tokenMatches and isValid after loadSession. - Redis: commandTimeout defaults to 5 s; TTL notes. - Known limitations: AuthFilter doesn't percent-decode; no TLS/sslmode options on the MySQL and PostgreSQL connectors; those drivers don't log database errors; MySQL token comparison follows the column collation. - Migrating from 3.x: PerfectSessionManager -> SessionDriver (new create requirement), couchDatabase/mongoCollection removed, cookieSameSite and CORS.methods type changes, the deprecated no-effect settings, shouldWeAccept -> requiresAuthentication. - From main: save renews idle, Redis 5 s timeout, per-key JSON dropping, AuthFilter and CSRF matching changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under ThreadSanitizer every exit test aborts with SIGABRT, even one whose body only calls exit(0) (checked on Xcode 27), so the three LogsDoNotContainTokens tests are skipped there, as the fd-starvation test already was. MySQL's timesPast32BitsRoundTrip compared updated across a save(), which now renews it; under Guard Malloc the slower run crossed a second boundary and failed. It now checks updated didn't go backwards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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.
Summary
Release prep for Perfect-Session 4.0.0, the first release since 3.1.5 (2018) and the last package in release wave 3. It pins Perfect-MySQL 4.0.0, Perfect-PostgreSQL 5.0.0, Perfect-Redis 4.0.0, Perfect-SQLite 6.0.0 and Perfect-MongoDB 4.0.3, and adapts the Redis backend to Redis 4.0.0's async actor API.
Two adversarial review rounds; the second came back clean, with nothing high or medium.
Security fixes
Session tokens. On Linux, Foundation's
UUID()returns an all-zero UUID once the process runs out of file descriptors, so every new session got the same token (reproduced). Tokens are now 32 bytes fromSystemRandomNumberGenerator, 64 hex characters.AuthFilter bypass. Under Perfect-NIO's case-insensitive router,
/Admin/usersreached a route protected as/admin. The newrequiresAuthentication(_:):/admincovers itself and everything below it but not/administrator;*only as a whole final segment;..,%,;or\, or when a rule is malformed.shouldWeAcceptis deprecated and now calls the new function.Redis keys. The cookie token was used as the bare Redis key, so a client could name any key in the database. Keys now carry a
perfect-session:prefix.Sessions coming back after logout. Saving after logout recreated the session on Redis and the in-memory driver. Redis now saves with
SET … XX.Expired sessions. Every backend's
resumenow refuses them; before, all five persistent backends accepted them. There's also an optionalabsoluteTimeout, whichclean(), the MongoDB TTL index and the Redis TTLs enforce.Session fixation. New
regenerate(). It keeps the session's creation time, so it can't be used to extend the absolute timeout.CSRF. A value that can't be stored as JSON (a
Date,URLorNaN) used to crash the process, and later wiped all session data including the CSRF token. Now only that key is dropped, and its name is logged.resumemakes sure a CSRF token exists. New constant-timeCSRFSecurity.tokenMatches.isValidnow compares scheme and default ports, ignores case, and rejects anullorigin.CORS.
https://*.example.comaccepted every https origin.Logs. Full tokens appeared in error logs; logs now show a 12-hex SHA-256 prefix instead.
Cookies. New
SessionCookiebuilder. It refuses values that would inject cookie attributes, and recognises the__Host-/__Secure-prefixes in any case.Correctness
saverenews the idle timeout on every backend. Before, only Redis did, contrary to the docs and to 3.x.idleofInt.max, both the overflow and the Redis TTL, which is now clamped;ALTER TABLEfor existing ones.commandTimeoutdefaults to 5 s (Tim's decision).CI and docs
SESSION_REQUIRE_LIVE,TZset and warnings as errors.Testing
160 tests, live against MySQL 8.4, PostgreSQL 17, Redis 7, MongoDB 8 and SQLite, with TZ=America/Denver and warnings as errors.
from: "4.0.0"Known limitations (in the notes)
TEXTcolumn holds 64 KiB of session data, and larger saves are lost.AuthFilterignores case using full Unicode rules. With Perfect-NIO 0.1.0 this can't be exploited, because NIO refuses non-ASCII request targets.regeneratecan leave the old token valid if destroying it fails.After merge: tag
4.0.0.🤖 Generated with Claude Code