Skip to content

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:

  1. 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.
  2. Validation coverage is per-parameter-plane, not per-query: where validated, join-internal sort not. The fix file validateSortQuery.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-Plus concatOrderBy legs on the Sept 18 query-grammar page).
  3. Join filters as an inference channel even without SQLi: polymorphic joins (join.on referencing 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 — try where[joinField.secretField][equals]=guess and diff result counts; then sweep greaterThan/contains for progressive extraction. This is the presence-oracle class generalized to predicate position.
  4. 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 %0A single-encoded after a leading /, plus %2509 double-encoding, %255C, %252f. Double-encoded whitespace is the part most startswith('/')+decode-once validators still miss.
  5. 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/:id came 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 its password consumed 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.
  6. "Disabled" settings must be verified at the router, not the config. The critical duplicate leg's true miss was ordering: disableDuplicate was 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 (POST with duplicateFrom-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/*.
  7. 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 _queryFilter leg.
  8. 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 isNestedRelationshipQuery prototype-identity check: distinguishing "plain object = sub-query" from class instances (ObjectId) by Object.getPrototypeOf is the fix shape — pre-fix, the parse itself was type-ambiguous.
  9. 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)

  1. Fingerprint adapter: request a where on 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.
  2. 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.
  3. Sort oracle: with a disposable low-priv account that can query one readable collection, set sort=<protectedField> (and the same inside each join's sort param). A 200 + deterministically-ordered results where field read returns 403 is the finding. Binary-search ranking only against synthetic values you planted yourself.
  4. Join predicate oracle: for each discovered join, run one equals guess 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.
  5. Redirect battery: replay the fix-commit test vector list (tab/CR/LF + double-encoded // prefixes) against every redirect= parameter in auth flows, including GraphQL login mutations; verify against Location headers with a URL parser that strips whitespace the way browsers do.
  6. 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).
  7. 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-0084bd5 abortOnLimit:false default — prove against your own canary file hash mismatch, not a real asset.
  8. 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/refresh and 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.
  9. 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 of disableDuplicate. Auth collections default this off at ≥3.90 — a 400 "cannot be duplicated" on the route (not a field filter) is the patched fingerprint.
  10. 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.
  11. API-key-in-read check (auth collections only): with account A reading account B's user document (where config permits), diff for apiKey/apiKeyIndex fields 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.
  12. 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 toLowerCase and array-rejection hunks are the proof that AND/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_path unvalidated (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/redis RediSearch 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:

  1. 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.
  2. 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].
  3. 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.
  4. 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.
  5. 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:

  1. Per-operator grammar battery, not per-parameter. The prior legs covered and/or case-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.
  2. 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.