Payload CMS wave (15 GHSAs): case-sensitive and/or query validation¶
Wave in one line: October 6 16:0xZ — case-sensitive and/or query validation, unvalidated join sort, auth-response and duplicate-path ACL misses, redirect whitespace-prefix drift, and multipart regex collapse
Source: hourly offensive-security scan of GitHub Security Advisories, 2026-10-06 (waves published 15:38Z, 16:09Z, and 16:17–16:22Z). Payload CMS (payload, Payload apps on the Drizzle adapters) published fifteen advisories in ~45 minutes. The fix commits are public, so each leg ships with its exact patched code shape — this is one of the richest same-product audit maps available right now.
Advisory table¶
| Leg | GHSA | CVE | CVSS | Boundary (from fix diffs + advisory text) |
|---|---|---|---|---|
| SQLi in SQLite/Postgres adapters | GHSA-v49j-62m6-pgrr | CVE-2026-105845 | 9.8 | Dynamic where filters/joins from untrusted users on readable collections reach SQL through the Drizzle adapter. Fix a742140 shows why validation missed: validateQueryPaths() recognized only lowercase and/or booleans — {'AND': [...]} walked the unknown-path branch — and an array-valued constraint on any other key was silently skipped. Patch: path.toLowerCase() + explicit errors.push({path}) for unvalidated arrays |
| Polymorphic join filters disclose hidden fields | GHSA-fpww-c55p-cjv6 | CVE-2026-105847 | high | Query access on a collection with a polymorphic join to sensitive fields lets you infer read-restricted field values, including password-reset tokens. Fix caa3f69 touches validateSearchParams.ts — join filter paths escaped per-field access validation |
| Sort queries expose protected field info | GHSA-9g87-32v6-3c2r | CVE-2026-105805 | medium | sort was field-access-checked on the main query but not inside joins — the fix adds a brand-new validateSortQuery.ts and wires it into sanitizeJoinQuery() next to the existing validateQueryPaths() call. Sort-by-protected-field = side-channel on values you cannot read |
| Stripe REST proxy access control | GHSA-r9v2-gg2j-22q5 | CVE-2026-105848 | medium | Authenticated user reaches the optional @payloadcms/plugin-stripe REST proxy and performs unintended Stripe operations — proxy forwards caller-selected method/path to Stripe with the server's key. Confused-deputy relay: your credential, your billing |
| MCP API key management access control | GHSA-2q76-m6w6-qgc6 | CVE-2026-105806 | high | Authenticated user manages MCP API keys outside their intended account → privilege escalation / account takeover via @payloadcms/plugin-mcp key objects |
| Prototype pollution → RCE in Import Export plugin | GHSA-qf28-8hc6-vwrp | CVE-2026-105844 | critical | Unauthenticated user when @payloadcms/plugin-import-export is enabled → unintended app behavior → RCE. Same commit a742140 touches the plugin's fieldPath.ts/getSelect.ts (import field-path resolution) and isEligibleRequest.ts — the multipart content-type gate's regex /multipart\/['"()+-_]+(?:; ?['"()+-_]*)+$/i had catastrophic backtracking (the eslint-disable comment admits it: regexp/no-super-linear-backtracking); the fix rewrites it to an anchored RFC-token class. Import pipeline: field-path traversal into select/where construction + gate regex that rejects expensive inputs rather than parsing them |
| Untrusted redirect after auth | GHSA-w84c-53h3-mc2g | CVE-2026-105846 | 6.1 | getSafeRedirect() accepted path prefixes that normalize off-origin after whitespace: /%09/example.invalid, /%0D/…, /%0A/…, and double-encoded /%2509/…, /%255C…, /%252f… variants. Fix rejects control-char and ambiguous encoded prefixes to fallback |
| Insufficient PBKDF2 iterations | GHSA-q6mq-ch85-c8mm | CVE-2026-105804 | medium | Low password-hash work factor; fix transparently upgrades hashes on successful login. Offline-cracking cost statement only — relevant when you already hold a hash dump |
| API key disclosure through ordinary document reads | GHSA-238x-w2j9-gwwr | CVE-2026-105849 | high | With useAPIKey enabled, any user with read access to other user documents got their active apiKey in the plaintext document read — an exposed key grants the target account's permissions until rotation. Fix 880d2e9 is a rebuild: apiKey/apiKeyIndex get read: () => false plus beforeRead/afterRead omit hooks, keys stored encrypted (encryptAPIKey), and an explicit opt-in /reveal-api-key endpoint gated on auth.useAPIKey.reveal === true — proving that pre-fix the base auth field had no read restriction at all |
| Field access control bypass on auth collections (duplicate) | GHSA-vc4h-q48j-5hcx | CVE-2026-105851 | critical | The duplicate operation copies field values from the source document even when the field is hidden or its access.read/access.create rule would reject that value for the caller — you read restricted fields by creating a duplicate of a document you can point at but not read. Fix 099ef12 shows the real miss: disableDuplicate (default on auth collections) was applied during config sanitize after the default endpoint list was already pushed — the duplicate REST endpoint existed regardless of the setting — plus a create-op duplicateFromID guard was added. Ordering bug in sanitize = "disabled" feature still routable |
| Token refresh / password reset responses expose restricted fields | GHSA-xgv3-crq2-6f69 | CVE-2026-105853 | high | refresh and forgot-password/reset-password responses returned full user documents without field-access or hidden-field filtering (read-route ACL applied only to the read routes). Fix f5f1283 also adds a collection-binding check the refresh op lacked entirely: req.user.collection !== collectionConfig.slug → 403 — a token from collection A was accepted by collection B's refresh endpoint |
| Relationship-query authorization bypass | GHSA-7c34-32v3-j575 | CVE-2026-105852 | medium | A readable collection with a relationship to a where-constrained protected collection leaked info about the protected docs. Fix 94059cf comment names the oracle verbatim: for has-many relationships, the user filter and the access constraint were checked independently, so "a different, readable document" could mask one the user cannot read (related-document oracle). Patch also validates nested contains relationship where against the related collection's fields, and classifies nested-query-vs-value by prototype identity (isNestedRelationshipQuery) so class instances like Mongo ObjectId aren't parsed as sub-queries |
| Password field update access not enforced | GHSA-fx49-4h83-wjv9 | CVE-2026-105855 | high | Field-level access.update on the auth password field was ignored: fix 9de9e79 moves password extraction to after the beforeValidate field-access pass — pre-fix shouldSavePassword was computed from raw data.password before the access hooks ever ran, so a special-cased credential path bypassed the generic field-ACL pipeline entirely |
| Stripe order confirmation replay | GHSA-8r29-2mp2-pmrw | CVE-2026-105850 | high | @payloadcms/plugin-ecommerce Stripe adapter: an order confirmation could be processed more than once — repeated settlement calls re-ran order creation, cart purchase, and inventory decrement. Fix 6c0c4dc adds an atomic pending → processing status claim per transactionID and idempotent retry returns — i.e., pre-fix the confirm endpoint was a replayable multi-settle primitive |
| Multipart Content-Type ReDoS | GHSA-2g7p-5934-q4w7 | CVE-2026-105854 | high | Malformed multipart bodies took "extremely long" to process. Fix 0084bd5 bounds it twice: new requestSizeLimit (default 50 MB request-wide) and abortOnLimit flipped false → true — the pre-fix default silently truncated oversized uploads instead of aborting. Paired with the import-export gate regex on the row above: the same wave proves both halves of the multipart-gate failure mode (slow parse + fail-soft truncation) |
Why one product's wave is an operator map¶
The fifteen legs share one architecture: Payload's generic find API takes user-controlled where (Mongo-style operators), sort, and joins, a query-validation layer is supposed to enforce per-field access before the database adapter builds SQL, and a field-ACL pipeline is supposed to guard every read/write verb. Every one of the read-side legs is the same failure with a different miss:
- Grammar-key case sensitivity in the validator (
AND≠and). The validator whitelists structural keys by exact string; the DB adapter's query builder treats them case-insensitively (SQL is). Anything the validator doesn't recognize is not rejected — historically it was skipped. Durable probe on any Mongo-grammar-over-SQL surface:{"AND":[...]},{"Or":[...]},{"and ":...}— if a filter that should fail path-validation succeeds, the validator and the executor disagree on the grammar. This is the Sept 18 query-grammar family's newest axis: not operator injection, structural-key normalization differential. - Validation coverage is per-parameter-plane, not per-query:
wherevalidated, join-internalsortnot. The fix filevalidateSortQuery.ts(83 new lines) did not exist before — sort fields simply never entered the field-access model inside joins. Sweep rule: for every query parameter plane a framework accepts (where,sort,limit,joins,depth,draft,locale, version params), ask which ones pass through field-level access validation; default assumption is the least-documented plane is unvalidated. Sorting by a protected column is an oracle: order results by the secret field, binary-search its value through rank observation — and Payload's join leg shows the same oracle reaching password-reset tokens (pair with the Sept 18 ClickHouse filter-key and MyBatis-PlusconcatOrderBylegs on the Sept 18 query-grammar page). - Join filters as an inference channel even without SQLi: polymorphic joins (
join.onreferencing sensitive collections) let a permitted query ask yes/no questions about read-restricted fields on the joined document. Field-level ACL protects GET output, not predicate participation. Operator test: any collection with a join to users/credentials — trywhere[joinField.secretField][equals]=guessand diff result counts; then sweepgreaterThan/containsfor progressive extraction. This is the presence-oracle class generalized to predicate position. - Redirect validation must normalize like the browser:
/\t//example.invalid— browsers strip tab/CR/LF from URLs, so/%09/%2f-prefixed "local paths" become protocol-relative. Payload's test additions enumerate the exact battery now worth keeping in any redirect harness:%09 %0D %0Asingle-encoded after a leading/, plus%2509 double-encoding,%255C,%252f. Double-encoded whitespace is the part moststartswith('/')+decode-once validators still miss. - Field ACL lives in the read verb only — enumerate the other verbs that return document data. The second wave (16:17–16:22Z) is one rule stated four ways: the same user document that filters fields on
GET /api/users/:idcame back unfiltered from token-refresh and password-reset responses (xgv3), copied wholesale by the duplicate operation (vc4h, critical), plaintext-embedded its own API key in any ordinary read because the base auth field never got a read rule (238x), and had itspasswordconsumed before the field-access pass ever ran because the auth op special-cased it early (fx49). Sweep rule: for any field-level ACL claim, list every endpoint whose response body or side effect embeds the document (auth flows, exports, duplicates, versions, webhooks, previews, bulk ops) and re-read the restricted field through each. This is the same sibling-route inversion as the Sept 19 WP alternate-surface page, now with server-side ORM proof. - "Disabled" settings must be verified at the router, not the config. The critical duplicate leg's true miss was ordering:
disableDuplicatewas assigned during sanitize after the default endpoint array was already pushed, so the duplicate REST route existed on every collection regardless. Fingerprint rule: probe the disabled-by-default route family directly (POSTwithduplicateFrom-style params) instead of trusting the config default; a feature-flag setting that only gates UI polish still leaves the REST surface. Same shape as Keycloak's disabled-feature-flag alternate routes and OpenAM's legacy/jaxrpc/*. - Session tokens must be bound to their collection/audience. The refresh fix added a check that simply did not exist:
req.user.collection !== collectionConfig.slug → 403. On any multi-auth-surface app (users + admins + partner portals), replay a token minted for collection A against collection B's refresh/me/logout family — cross-family token acceptance is a standing probe, sibling of the Oct 3 OpenAM cross-realm_queryFilterleg. - Has-many relationship filters are independent-constraint oracles. The 94059cf comment is the cleanest statement of the class: when the caller's filter and the access constraint are AND-ed loosely across a has-many relationship, any one readable related document can satisfy one half while a hidden one satisfies the other — you infer properties of documents you cannot read by watching whether the readable row "masks" the query. Related-document pairing of the join-predicate oracle in item 3. Also note the
isNestedRelationshipQueryprototype-identity check: distinguishing "plain object = sub-query" from class instances (ObjectId) byObject.getPrototypeOfis the fix shape — pre-fix, the parse itself was type-ambiguous. - Payment confirmations are state machines; replay them. The ecommerce leg shows a confirm endpoint without an atomic pending→processing claim = replayable multi-settle (repeat order creation/cart purchase/inventory decrement). Durable check on any checkout/credit/wallet flow: fire the confirmation twice concurrently with the same transaction reference and diff side effects (second order? double inventory decrement? double grant?). Idempotency-key absence is fingerprintable before you even have a payment method.
Plugin-surface rule¶
Four of the fifteen legs are plugins (stripe, mcp, import-export, ecommerce), all disabled in default installs. Payload's own impact statement leads with "Applications that do not use X are not affected" — which inverts into the fingerprinting rule: on a Payload target, enumerate enabled plugins first (/api/plugin-import-export/*, Stripe proxy routes, MCP key endpoints, ecommerce checkout/confirm routes exist only when mounted) before spending time on core. The unauthenticated-RCE leg needs only the import-export plugin enabled — one optional dependency turns a clean install into an unauth attack surface. Same lesson as the June 19 Langflow connector loop: plugin inventory is the first recon phase on plugin-based CMSes.
Validation workflow (authorized scope only)¶
- Fingerprint adapter: request a
whereon a json/richText field and compare Postgres/SQLite vs Mongo error shapes; the SQLi leg is Drizzle-only. Version-fingerprint per-advisory: patched bands are 3.88.0 (SQLi, import-export, sort, redirect) and 3.90.0 (join disclosure, Stripe proxy, PBKDF2, and the entire 16:17–16:22Z auth wave: API-key disclosure, duplicate bypass, refresh/reset field leak, relationship oracle, password-update access, ecommerce replay, multipart bounds) — a target can sit between the two bands and still leak reset tokens. - Grammar-differential probe: submit a filter with
{"AND":[{"secret.path":{"equals":"x"}}]}vs the lowercase form. Expect identical 400-path-validation from ≥3.88, and on vulnerable builds a behavior difference (200 vs 400, or results vs QueryError) proving the structural key escaped validation. Synthetic paths only; never probe real credential fields beyond your own canary collection. - Sort oracle: with a disposable low-priv account that can query one readable collection, set
sort=<protectedField>(and the same inside each join'ssortparam). A 200 + deterministically-ordered results where field read returns 403 is the finding. Binary-search ranking only against synthetic values you planted yourself. - Join predicate oracle: for each discovered join, run one
equalsguess against a joined sensitive field you planted (canary reset token), record count>0 vs count=0. Prove inference, do not exfiltrate real tokens; report the count-differential artifact. - Redirect battery: replay the fix-commit test vector list (tab/CR/LF + double-encoded
//prefixes) against everyredirect=parameter in auth flows, including GraphQL login mutations; verify againstLocationheaders with a URL parser that strips whitespace the way browsers do. - Plugin enumeration: unauthenticated requests to import-export preview/export endpoints, the Stripe proxy path, and MCP key CRUD — decision table (route status + response shape) per plugin, with and without a lowest-tier account, plus the cross-account MCP key check using two disposable users (never live keys).
- Multipart ReDoS check (import-export only): a
Content-Type: multipart/form-data; boundary=;;;;...!with ~50 semicolons should return a fast 4xx on patched builds; multi-second response on the gate = pre-fix regex; run once against your own lab instance only (availability impact). Companion check on core uploads: an oversized upload that returns 2xx with a silently truncated file = pre-0084bd5abortOnLimit:falsedefault — prove against your own canary file hash mismatch, not a real asset. - Auth-response field sweep: with a disposable pair of accounts (one can read the other's document if the config allows), call
POST /api/users/refreshand the forgot/reset flow with an expired-but-parseable JWT and record which fields appear in the returned user object vs the same user's filtered read response. Restricted/hidden fields present in refresh/reset bodies = CVE-2026-105853 shape. Redact everything but field names in evidence. - Duplicate-verb probe: with the lowest-tier account, attempt the duplicate operation (
duplicateFrom-style param on the create route) against a document containing a field your role should not read (self-planted canary). A created copy carrying the canary value proves the ACL-free copy path regardless ofdisableDuplicate. Auth collections default this off at ≥3.90 — a 400 "cannot be duplicated" on the route (not a field filter) is the patched fingerprint. - Cross-collection token replay: mint a token on auth collection A, then call collection B's
refresh/me/logout family with it. Acceptance (200 + B-scoped identity) = missing collection binding (CVE-2026-105853's sibling check). Two disposable auth collections in your own lab instance. - API-key-in-read check (auth collections only): with account A reading account B's user document (where config permits), diff for
apiKey/apiKeyIndexfields in the response body. Present = pre-fix base-field config; report only field presence against your own seeded key, then rotate it. Never retain another account's live key in evidence. - Confirm-replay check (ecommerce plugin only, authorized lab): fire the Stripe order-confirmation leg twice concurrently with the same transaction reference and diff side effects — second order row, second inventory decrement, or double cart-purchase flag = missing atomic claim. Lab store with synthetic products only; never against live payment credentials.
Reporting checklist¶
- Label adapter + plugin state per target; the same Payload version is four different attack surfaces depending on DB adapter and enabled plugins.
- Separate the claims: sort-disclosure (ordering side-channel) ≠ join-predicate inference (yes/no oracle) ≠ SQLi (adapter-level). Each needs its own evidence artifact; do not bundle as "API access control bypass".
- For reset-token inference, prove with a token you generated and redact the value; report only the count-differential trace.
- Cite the fix commit alongside each leg — the
toLowerCaseand array-rejection hunks are the proof thatAND/array-constraint filters previously bypassed path validation.
Tracked without publication (same waves)¶
- Vyper memory/stack corruption trio (function calls in tuples, complex-type args, nested
return) — compiler/codegen memory-safety, no operator axis yet. - MsQuic OpenSSL/QuicTLS backend missing TLS hostname verification → on-path server-identity spoof (folded to the Oct 2 cert page).
- Kiota doc-comment delimiter reformation +
oauth_card_pathunvalidated (folded to the July 24 Kiota page). - MCP TypeScript SDK credentials-to-attacker-named-authorization-server relay (folded to the Sept 16 rmcp/OAuth page).
- LangChain
@langchain/redisRediSearch filter injection (folded to the Sept 18 query-grammar page). - External Secrets Operator webhook-generator label-enforcement init-order bypass → secret exfil to attacker URL (folded to the Sept 18 ignored-scope page).
- i18next-http-backend colon-scheme SSRF via
{{lng}}interpolation (folded to the Sept 15 parser page). - openapi-python-client arbitrary code generation from malicious OpenAPI documents — class-covered by the Kiota/amplify generated-source family on the July 24 page; revisit if fix guardrail details land.
October 7 16:1xZ follow-up: MongoDB-adapter field-write ACL bypass via non-object rows (CVE-2026-106100 / GHSA-4ww4-68q3-h7g5, 7.1, fixed 3.87.0 / 4.0.0-canary.20)¶
Sixth wave on this product. beforeValidate/promise.ts walked Array.isArray(rows) rows straight into traverseFields() with field-access enforcement keyed on row shape — null, primitives, and nested arrays in an array/blocks field input skipped per-field access validation (and could crash downstream row.blockType = writes); the fix adds stripNullRows() filtering to plain objects before traversal. Drizzle adapters unaffected — same API, different adapter, different ACL outcome. Operator rules: (1) on any array/blocks field with field-level write ACL, probe row-element types ([null, {...}], ["x", {...}], [[...], {...}]) not just field spellings — element-shape is a third plane alongside key-case (the AND leg) and array-vs-object constraints; (2) adapter parity is a mandatory decision table on multi-adapter products: run the identical payload against MongoDB and Postgres deployments/staging and diff, since a passing ACL test suite on one adapter proves nothing about the other. Fingerprint band now: <3.87.0 vulnerable to this leg, <3.88/3.90 bands for the Oct 6 legs.
October 7 20:2xZ follow-up: third Payload wave (thirteen GHSAs, CVE-2026-105856…105868, all fixed 3.90.0 / 4.0.0-canary.34)¶
Third coordinated wave on this product in ~28 hours, published 20:28–20:43Z. Fix diffs read for the top legs. Advisory table (boundary from public fix commit or advisory text):
| Leg | GHSA / CVE | CVSS | Boundary |
|---|---|---|---|
| SQLi via JSON path segments (SQLite/Postgres) | GHSA-pj7x-6wpf-pgvp / CVE-2026-105856 | high | Second SQLi on this page, different sink: createJSONQuery() interpolated where path segments for json / blocksAsJSON fields into SQL JSON-path expressions. Fix 03b78c7e replaces parseInt-based numeric detection with a strict parseJSONPathSegment() (/^\d+$/ + sanitize, typed objectKey-vs-arrayIndex) — pre-fix the numeric branch classification (!Number.isNaN(parseInt(seg))) accepted segment spellings that then flowed into the traversal string. Requires read + create/update on a collection with a json field. richText explicitly unaffected |
| RCE through first-register | GHSA-97rh-rhh2-7vjv / CVE-2026-105858 | 8.1 | Crafted request to the public create-first-user operation while no initial user exists → RCE (fix e947fc4b "properly sanitizes" the submitted document). The same commit shows a standing gate bug: pre-fix isPublicAdminRoute() returned public for any route containing the substring /verify/; post-fix it matches exactly <authCollection>/verify/<id> segments. Pre-fix, /verify/ appearing anywhere in an admin-scoped route skipped auth entirely — a fingerprintable alternate-path family |
| Unauthorized update via orderable endpoint | GHSA-f7hx-52q9-hcrf / CVE-2026-105859 | 9.8 | Fix 36fa9af7 shows the exact pre-fix shape of the reorder endpoint: update-access was executed once, on docsToMove[0] only, with data: {}, and no per-document where-result ACL re-check (the fix adds assertUpdateAccess() per doc + combineQueries(id, accessResult) lookup), and no check that the requested orderable field is even configured (fix adds a 400 gate). Pre-fix: reach orderableFieldName params for arbitrary fields and update documents the coarse check shouldn't have covered. Trigger: any collection or join field with orderable: true |
| Form Builder RCE | GHSA-r488-j9vj-wx3q / CVE-2026-105857 | 10.0 | Crafted form submissions execute code server-side (@payloadcms/plugin-form-builder, submission create: () => true — unauthenticated ingress). Adjacent hunks in the same commit show the pre-fix read gates on submissions and form emails arrays were !!user — any logged-in user (any collection) could read form submissions and email config; post-fix req.user?.collection === config.admin.user |
| Multi-tenant plugin authz pair | GHSA-p96c-xwx8-3cqj / CVE-2026-105860; GHSA-xhm9-gwgw-3q2q / CVE-2026-105864 | high | Default tenants-array fieldAccess lets an authenticated user assign themselves to other tenants; and tenant-scoped create never verifies membership, so a single-tenant user can create records inside another tenant (reads/edits unaffected — create-only bypass) |
| Client uploads overwrite other collections' S3 objects | GHSA-7vg8-29qx-jgj8 / CVE-2026-105867 | high | Multiple upload collections sharing one bucket without useCompositePrefixes: client-supplied upload keys collide across collection boundaries → overwrite another collection's object, bypassing its access control and file validation |
| SVG sanitization bypass + XML/XSL same-origin JS | GHSA-2pwp-2369-8fg3 / CVE-2026-105862 (8.7); GHSA-9qpg-3cf8-w33x / CVE-2026-105868 | high | SVG sanitize bypass on some upload paths → stored XSS on download+open (fix "validates SVG content on every upload path" — per-path sanitize parity). And uploaded XML + attached stylesheet executes JS on the Payload origin when a logged-in user opens it (XML uploads accepted by default) |
| External upload forwards session auth to unverified destination | GHSA-pj5h-5q6c-3pfx / CVE-2026-105861 | high | URL-based upload retrieval sent auth headers to an unverified destination, and trust was not re-validated when the request changed destination (redirect) → session token to unintended recipient. Fix re-validates per-destination after redirect |
| Auth token reserved-claim collision | GHSA-66wr-7vmr-p5jq / CVE-2026-105863 | critical | A custom field whose option maps to a reserved authentication claim name had its value included in the login-issued token — user data injected into the auth token's claim namespace. Fix restricts which field options can influence token contents |
| Upload lifecycle file cleanup escape + lockout DoS | GHSA-p223-2wr2-j562 / CVE-2026-105865 (8.1); GHSA-v5gf-vpjc-pc7w / CVE-2026-105866 | high/med | Filename validation on cleanup let an upload-permitted user delete files outside the upload dir (destructive — lab-only proof); and unauthenticated forgot-password churn triggers account lockout (fix: reset clears lockout + 15 s email interval) — lockout state doubles as an account-existence oracle |
Durable rules from this wave:
- Substring-matched public-route gates are alternate-path surfaces. The pre-fix
routeWithoutAdmin.includes('/verify/')is the cleanest example yet: a "public auth route" check written as substring containment makes every admin-scoped URL containing that fragment unauthenticated. On any Payload <3.90 admin panel, probe admin-scoped routes with/verify/injected into the path (e.g. as a filename or extra segment) before authenticating, and expect 401-vs-200 differentials that expose data you were never issued a token for. Generalizes the page's "disabled-setting-at-router" rule to route-classifier grammar. - Coarse pre-flight access checks are not per-document ACL. The orderable endpoint checked update access once, on the first ID, with empty data — the standard shape of a batch endpoint's auth bug. Sweep rule for any reorder/bulk/relation-set endpoint: vary the document set (put an ACL-excluded doc at position >0) and vary the field name parameter; expect the check to cover every mutation, not just
docs[0]. - Reserved-claim collision in login tokens. Inventory every custom field whose name or option mapping matches a reserved JWT claim (
roles,collection,id,exp, tenant/plugin claims); a field that lands in the token payload is privilege-relevant, not cosmetic. Check token contents with your own disposable account's JWT decoded — never another user's. - Per-path sanitize parity (SVG leg): the fix wording ("validates on every upload path") names the pre-fix miss class — sanitizer wired into the main upload handler but not the secondary paths (import, API-direct, programmatic create). Upload the same canary SVG through every ingress (form, REST create, import-export, external-URL fetch) and diff stored bytes. Same parity rule as the page's auth-response sweep, applied to sanitizers instead of ACL.
- Third-wave cadence is itself fingerprintable: three coordinated bands in one day, all fixed at 3.90.0/canary.34, means a build between 3.87 and 3.90 still has the Oct 6 auth-wave legs plus everything above. Probe band boundaries with the route-existence fingerprints (orderable endpoint 400-gate added at 3.90;
/verify/matching shape; orderable field-name 400) rather than trusting version banners.
Validation workflow (authorized scope only): orderable probe = two disposable users with row-scoped update ACL, move a canary doc each cannot update via the reorder endpoint, prove field/row write on your own collection only; first-register leg = fresh disposable install only (a deployed Payload instance with zero users is a race you win by registering first — for your own infra recon, treat "admin panel answering, no users yet" as a take-it-first signal); S3 overwrite = two disposable upload collections sharing a lab bucket; auth-claim check = decode your own login JWT against the field inventory. Keep destructive cleanup-escape proof to a marker file you created outside the upload dir in a lab.
October 13 follow-up: fourth Payload wave leg — in/not_in query-operator SQL injection (CVE-2026-63316 / GHSA-57gg-gh75-cfjg, high, fixed 3.90.0 / 4.0.0-canary.34)¶
Published in the 2026-10-13T12:32Z wave, same fix band as the Oct 6 third wave (3.90.0/canary.34): authenticated SQL injection through improper neutralization of the in and not_in query parameters. This lands on the same boundary this page already maps — Payload's query layer translates operator structures into SQL — but names the specific operator pair that escaped the earlier validation passes: list-membership operators whose values are split/serialized into SQL value lists rather than bound individually.
Operator update to this page's sweep kit:
- Per-operator grammar battery, not per-parameter. The prior legs covered
and/orcase-sensitivity, sort/orderable, and join paths; this leg shows validation coverage is per query operator. For any Payload ≤3.89 instance (or a banner between 3.87 and 3.90): exercise every filter operator the docs list —equals, not_equals, in, not_in, contains, like, greater_than, less_than, exists, all, near— with value shapes that probe SQL serialization: comma lists, quotes,--,), stacked-separator candidates, percent/underscore for LIKE paths. Diff the generated SQL/error shape per operator against your own disposable collection only. - Fix-band fingerprinting continues to pay: the 3.90.0/canary.34 band now carries three waves of query-layer fixes; an instance answering on 3.87.x has the Oct 6 auth legs and this SQLi leg. Route-existence/behavior fingerprints (orderable 400-gate,
/verify/substring gate) still identify the band without trusting the banner.