You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Changelog entries for the three PRs merged into main: the per-recipient
bound on verification emails (#39), the client-IP resolution fix (#40),
and mailbox-keyed rate limits (#41).
Both security items are listed under Security rather than folded into
Fixed, since each was exploitable: replayed credentials from an
unverified account could drive unlimited mail at the registered address,
and the activity log recorded whatever address a caller claimed. The
removal of the change_email_rate_limit_* options is called out
explicitly, because sites that set them lose them.
Version bumped in all five places that carry it, including
src/WorkOS/Plugin.php, which 1.0.8 missed. That property backs
WORKOS_VERSION, so leaving it stale makes the runtime disagree with the
plugin header and the .org listing. Note that AGENTS.md documents four
locations and omits package.json, and .puprc cannot help here because it
targets a workos.php that does not exist.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+27Lines changed: 27 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,32 @@
1
1
# Changelog
2
2
3
+
## [1.0.9] - 2026-08-05
4
+
5
+
### Security
6
+
7
+
-**Unlimited verification emails from the sign-in endpoint** (#39) — an account that signed up and abandoned email verification left a working credential behind, and replaying it against `POST /wp-json/workos/v1/auth/password/authenticate` made WorkOS mail a fresh verification code every time. The only bound was 5 per minute against the requester, which is sustainable indefinitely, and the transient counter behind it was non-atomic so a concurrent burst passed straight through. Email sends are now charged against the recipient across layered windows: 3 per 5 minutes, 5 per hour, and 10 per 8 hours. The sign-in path reserves a slot before calling WorkOS and refunds it when the response shows nothing was sent, so a failed password or a completed login costs the recipient nothing. Magic-code send and password-reset start share one recipient bucket, so rotating between endpoints buys no extra mail.
8
+
-**Activity log recorded forgeable IP addresses** (#40) — the log read forwarded-for headers without checking that the request had actually passed through a proxy, and took the left-most entry, which is the portion a client writes. Any caller could attribute their own actions to an address of their choosing, which made the log unusable as evidence for the thing it exists to record. The address the webserver saw is now the default, and it cannot be forged.
9
+
10
+
### Added
11
+
12
+
-**`WORKOS_CLIENT_IP_HEADER` constant** (#40) — names the proxy header to trust on sites behind a CDN. Setting it is a deliberate assertion that the origin accepts nothing but proxied traffic, because an origin reachable directly makes the header attacker-controlled again.
13
+
14
+
### Changed
15
+
16
+
-**Rate limits count the mailbox, not the string as typed** (#41) — `alice@gmail.com`, `alice+1@gmail.com`, and `a.l.i.c.e@gmail.com` each earned a separate allowance against a single inbox, which was enough to type past the cap. Anything whose effect is mail now counts the mailbox an address lands in. Rewrites apply per provider rather than universally: merging two real mailboxes would let either starve the other, and `business+brian@corp.com` is routinely its own mailbox with its own owner. The address sent to WorkOS is untouched; the reduced form is only ever a counter key, never a destination.
17
+
-**Guessing limits still count the address exactly as typed** (#41) — WorkOS keys accounts on the exact string, so `alice@` and `alice+work@` can both exist with separate passwords. Merging them would let an attacker on one lock out the other, so password and code attempts keep the literal address as their subject.
18
+
-**One answer to "who is calling"** (#40) — rate limiting, the activity log, and the audit log each had their own IP resolution and could name three different callers for one request. They now share one implementation.
19
+
-**Rate limiting moved into its own domain** (#39) — a single-window interface with transient and object-cache implementations, plus a wrapper that stacks windows into a policy. Previously this lived inside the AuthKit package while being used across the plugin.
20
+
21
+
### Removed
22
+
23
+
-**`change_email_rate_limit_*` options** (#39) — the change-email flow now uses the shared tiered limiter. Sites that set these will lose them; the layered send policy replaces them.
24
+
25
+
### Fixed
26
+
27
+
-**Concurrent-burst bypass on rate limits** (#39) — sites with a persistent object cache now get atomic counters. Sites without one keep the read-modify-write path, which the longer window tiers bound.
28
+
-**Magic-code verify accepted malformed addresses** (#41) — it took any non-empty string; it now rejects addresses that are not valid.
@@ -175,6 +175,17 @@ WorkOS is provided by WorkOS, Inc.
175
175
176
176
== Changelog ==
177
177
178
+
= 1.0.9 - 2026-08-05 =
179
+
180
+
* Fix: Unlimited verification emails from the sign-in endpoint. Credentials from an account that never finished email verification could be replayed to make WorkOS send a fresh code every time, bounded only at 5 per minute against the caller. Email sends are now counted against the recipient across 5 minute, 1 hour, and 8 hour windows, and a reserved slot is refunded when the response shows nothing was sent. (#39)
181
+
* Fix: The activity log no longer records forgeable IP addresses. Forwarded-for headers were trusted without a proxy in front, and the left-most entry was used, so any caller could attribute their actions to an address of their choosing. The address the webserver saw is now used by default. (#40)
182
+
* New: `WORKOS_CLIENT_IP_HEADER` constant names a trusted proxy header for sites behind a CDN. Only set it when the origin accepts nothing but proxied traffic. (#40)
183
+
* Fix: Rate limits now count the mailbox an address lands in, so plus-tags and dots on providers that ignore them no longer multiply a single inbox's allowance. The address sent to WorkOS is unchanged. Sign-in and code guessing still count the address exactly as typed, since WorkOS treats those as separate accounts. (#41)
184
+
* Fix: Rate limiting, the activity log, and the audit log now agree on the caller's IP address instead of each resolving it separately. (#40)
185
+
* Fix: Sites with a persistent object cache now get atomic rate-limit counters, closing a bypass where concurrent requests passed the same check. (#39)
186
+
* Fix: Magic-code verify rejects malformed email addresses instead of accepting any non-empty string. (#41)
187
+
* Fix: Removed the `change_email_rate_limit_*` options. The change-email flow now uses the shared tiered rate limiter. (#39)
188
+
178
189
= 1.0.8 - 2026-06-30 =
179
190
180
191
* New: Change a user's email from the WorkOS → Users admin page, alongside "Open in WorkOS" and "Send password reset". (#33)
@@ -272,6 +283,9 @@ Base platform:
272
283
273
284
== Upgrade Notice ==
274
285
286
+
= 1.0.9 =
287
+
Security release. Closes an unlimited-verification-email path on the sign-in endpoint and stops the activity log trusting spoofable IP headers. Rate limits now count the mailbox an address lands in, so plus-tags no longer multiply an inbox's allowance. Removes the `change_email_rate_limit_*` options in favour of the shared tiered limiter; sites behind a CDN should set `WORKOS_CLIENT_IP_HEADER`.
288
+
275
289
= 1.0.8 =
276
290
Adds a "Change email" action to the WorkOS → Users admin page and makes admin-of-other email changes commit immediately, while self-service keeps the verified token flow. Also fixes a 500 in the admin password-reset endpoint when the profile field is empty.
0 commit comments