The anonymous share-link gate looks a share id up without checking which channel it belongs to, and the Playground channel mints its id deterministically. So the unguessable-token assumption the gate rests on does not hold for that channel.
packages/service/support/permission/publish/authLink.ts:72:
const outLinkConfig = await MongoOutLink.findOne({ shareId }).lean<OutLinkSchemaType<T>>();
No channel discriminator. Its own sibling thirty lines below, loadOutlinkProviderConfig at :105, does pin it with type: channel. The gate is safe for real share links because their ids are unguessable: outLink/create.ts:33 uses getNanoid(24) and the route requires ManagePermissionVal.
Playground breaks that assumption. projects/app/src/pages/api/support/outLink/playground/update.ts:31,42 requires only WritePermissionVal and writes shareId: \playground-${appId}`. That row is not a publication: the Playground panel's own link is ${origin}/chat?appId=${appId}&pane=..., the logged-in internal page, and the row exists to hold preview toggles whose stored defaults are permissive (showCite, showFullText, canDownloadSourceandshowWholeResponse` all true).
An owner creates a private app, never publishes it, and saves Playground visibility once. An anonymous caller with no session and no key derives playground-<appId>:
[outLink rows created] [{"shareId":"playground-6a9b0313...b453","type":"playground"}]
[ANON outLink/init] code= 200 error= undefined
"name": "VictimPrivateApp", "intro": "CONFIDENTIAL-INTRO-MARKER",
"type": "simple", "chatConfig": {...}
[CONTROL bogus shareId] code= 500 error= "linkUnInvalid"
The control matters: an invented share id is rejected, so this is not a blanket pass. The route is GET /api/core/chat/outLink/init through authOutLink to authOutLinkValid.
The caller starts with zero privilege and needs only an app id, which appears in /app/detail?appId= and in the panel's own copyable link.
Suggested CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N, 5.3, for what I actually ran.
Not executed and therefore not claimed: the escalation through authOutLinkChatStart (auth/outLink.ts:43), which returns the victim's teamId and tmbId and would mean workflow execution billed to the victim. I traced that by reading only. If it holds it is materially worse, and it should be run before anyone leans on it.
On how this relates to GHSA-q4pr-3qpg-9q5v: that advisory told you authOutLinkValid is not an authorization boundary, and you fixed it by moving the two WeChat endpoints to authOutLinkCrud, which I confirmed at HEAD, while deliberately leaving authOutLinkValid as the anonymous gate. That is a reasonable design and this is not that bug. The new element is that playground-${appId} makes the bearer token predictable, which collapses the precondition from knowing a published 24-character nanoid to knowing an app id.
Introduced in 82fa431a (2026-06-06, PR #7070), first tag v4.15.0-beta4, confirmed present at released v4.16.2 and at HEAD c311ae9e. Not in your other 34 published advisories, not in .agents/issue/, and not in open PRs #7559 or #7652.
On how I found it and a note on tooling: I was checking which callers of the anonymous gate could have a predictable share id. I used AI assistance while investigating, but I set up the victim app and made the anonymous request myself, including the negative control, so the transcript above is measured.
The anonymous share-link gate looks a share id up without checking which channel it belongs to, and the Playground channel mints its id deterministically. So the unguessable-token assumption the gate rests on does not hold for that channel.
packages/service/support/permission/publish/authLink.ts:72:No channel discriminator. Its own sibling thirty lines below,
loadOutlinkProviderConfigat:105, does pin it withtype: channel. The gate is safe for real share links because their ids are unguessable:outLink/create.ts:33usesgetNanoid(24)and the route requiresManagePermissionVal.Playground breaks that assumption.
projects/app/src/pages/api/support/outLink/playground/update.ts:31,42requires onlyWritePermissionValand writesshareId: \playground-${appId}`. That row is not a publication: the Playground panel's own link is${origin}/chat?appId=${appId}&pane=..., the logged-in internal page, and the row exists to hold preview toggles whose stored defaults are permissive (showCite,showFullText,canDownloadSourceandshowWholeResponse` all true).An owner creates a private app, never publishes it, and saves Playground visibility once. An anonymous caller with no session and no key derives
playground-<appId>:The control matters: an invented share id is rejected, so this is not a blanket pass. The route is
GET /api/core/chat/outLink/initthroughauthOutLinktoauthOutLinkValid.The caller starts with zero privilege and needs only an app id, which appears in
/app/detail?appId=and in the panel's own copyable link.Suggested
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N, 5.3, for what I actually ran.Not executed and therefore not claimed: the escalation through
authOutLinkChatStart(auth/outLink.ts:43), which returns the victim'steamIdandtmbIdand would mean workflow execution billed to the victim. I traced that by reading only. If it holds it is materially worse, and it should be run before anyone leans on it.On how this relates to GHSA-q4pr-3qpg-9q5v: that advisory told you
authOutLinkValidis not an authorization boundary, and you fixed it by moving the two WeChat endpoints toauthOutLinkCrud, which I confirmed at HEAD, while deliberately leavingauthOutLinkValidas the anonymous gate. That is a reasonable design and this is not that bug. The new element is thatplayground-${appId}makes the bearer token predictable, which collapses the precondition from knowing a published 24-character nanoid to knowing an app id.Introduced in
82fa431a(2026-06-06, PR #7070), first tag v4.15.0-beta4, confirmed present at released v4.16.2 and at HEADc311ae9e. Not in your other 34 published advisories, not in.agents/issue/, and not in open PRs #7559 or #7652.On how I found it and a note on tooling: I was checking which callers of the anonymous gate could have a predictable share id. I used AI assistance while investigating, but I set up the victim app and made the anonymous request myself, including the negative control, so the transcript above is measured.