Security findings 11
Data exfiltration
criticalConfirmedhigh confidenceThe ChatGPT adapter's buildChatGPTBackendHeaders harvests the browser's entire cookie store via an unfiltered page.getCookies() (including cookies for non-ChatGPT domains), serializes them into a Cookie header, and sends them in a fetch to chatgpt.com/api/auth/session to extract an access token.
page.getCookies().catch(() => []),
vector:code
Counter-argument considered
The unfiltered getCookies() sweep is serialized into the Cookie header sent to chatgpt.com/api/auth/session to extract an access token, so cookies for non-ChatGPT domains flow off their origin; no carve-out, declaration, or mitigation applies.
Credential access
highContextual riskmedium confidencebuildChatGPTConversationHeaders in the shipped ChatGPT adapter merges cookies from an unfiltered page.getCookies() call, alongside the four chatgpt.com-scoped calls, into the Cookie header it transmits to chatgpt.com, so cookies from unrelated sites in the browser profile may be sent off their origin, an undeclared capability beyond the skill's stated adapter-authoring purpose.
page.getCookies().catch(() => []),
vector:code
Counter-argument considered
The unfiltered page.getCookies() merges profile-wide cookies (not just chatgpt.com ones) into the Cookie header transmitted to chatgpt.com; four scoped calls already exist, and no scoping, declaration, or mitigation covers sending foreign-domain cookies off their origin.
Data exfiltration
highContextual riskhigh confidencebuildChatGPTConversationHeaders duplicates the same pattern, harvesting all browser cookies with an unfiltered page.getCookies() and sending them to chatgpt.com's session endpoint to obtain a bearer access token.
page.getCookies().catch(() => []),
vector:code
Counter-argument considered
The same unfiltered harvest feeds the Cookie header sent to chatgpt.com/api/auth/session to obtain a bearer access token, disclosing cookies from unrelated domains to that endpoint; nothing declares or mitigates the cross-origin transmission.
Credential access
highContextual risklow confidenceUndeclared for the adapter-authoring skill, clis/chatgpt/utils.js collects cookies through an unfiltered page.getCookies() sweep (in addition to chatgpt.com-scoped filters) and merges every returned cookie by name into a single Cookie header that is later transmitted to chatgpt.com by buildChatGPTBackendHeaders/setChatGPTModelConfig, so cookies not scoped to the wrapped service can be included; the chatgpt.com session fetch (R8) and settings PATCH (R9) themselves are the ChatGPT adapter contacting its own service and are adjudicated necessary.
page.getCookies().catch(() => []),
vector:code
Counter-argument considered
buildChatGPTBackendHeaders adds every non-colliding cookie from an unfiltered page.getCookies() into one Cookie header later transmitted to chatgpt.com's session endpoint; the four chatgpt.com-scoped calls show the sweep is unnecessary, and no declaration or carve-out covers sending foreign-domain cookies off their origin.
Data exfiltration
highContextual risklow confidenceThe Hupu adapter's buildBrowserJsonPostScript harvests device/fingerprint identifiers from browser cookies and localStorage (sensorsdata2015jssdkcross device_id, ua→puid, smidV2→shumei_id, .thumbcache_) and POSTs them with credentials:'include' to any caller-supplied apiUrl, so a non-Hupu apiUrl would transmit Hupu session-adjacent device identifiers to a third party; the served callers that would fix the destination were not shown.
const sensorsRaw = decodeURIComponent(getCookie('sensorsdata2015jssdkcross') || '');vector:code
Counter-argument considered
The shipped script harvests device/fingerprint identifiers and issues a credentials:'include' POST from the logged-in Hupu page to a caller-supplied apiUrl; no unit restricts the destination and no declaration or mitigation covers this parameterized credentialed sink, so the finding stands.
Credential access
highContextual risklow confidencebuildBrowserJsonPostScript harvests the live Hupu session's cookies and localStorage device identifiers (thumbcache, sensorsdata device_id, ua, smidV2) inside the logged-in page and embeds them in a credentials-included POST to a caller-supplied apiUrl (with a mode='reply' write path), so session-derived identifiers flow to a destination this artifact does not show.
const getCookie = (name) => document.cookie
vector:code
Counter-argument considered
The same script reads live session cookies and localStorage device identifiers into a credentials-included POST whose apiUrl is a free parameter; nothing shown scopes the sink to Hupu or declares the pattern, so the finding stands.
Data exfiltration
highContextual riskmedium confidenceUndeclared residue R16: the jianyu search adapter (existing adapter shipped with this adapter-authoring skill) sends the user's search query to the third-party proxy r.jina.ai, which proxies DuckDuckGo HTML, as a fallback index fetch - a destination beyond jianyu360.cn that the declared purpose does not cover and the file header does not mention.
const SEARCH_INDEX_PROXY = 'https://r.jina.ai/http://duckduckgo.com/html/?q=';
vector:code
Counter-argument considered
The shipped jianyu adapter sends user search queries to the third-party proxy r.jina.ai, a destination outside the wrapped jianyu360.cn service that the file's own header does not declare; the query-to-proxy chain is concrete and no carve-out or mitigation applies.
Credential access
highContextual riskmedium confidencesaveTokens persists Spotify OAuth access and refresh tokens to ~/.opencli/spotify-tokens.json via writeFileSync without an explicit restrictive mode, leaving the credential file's permissions to the default umask (typically 0644, group/world-readable).
writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2));
vector:code
Counter-argument considered
OAuth access/refresh tokens are written to ~/.opencli/spotify-tokens.json with no explicit restrictive mode, so exposure rides on the default umask; no unit sets a mode or umask, no declaration covers the token file, and no carve-out covers persisting wrapped-service tokens potentially world-readable.
Credential access
highUnverifiedlow confidenceThe Spotify adapter resolves SPOTIFY_CLIENT_ID/SPOTIFY_CLIENT_SECRET from process.env or the ~/.opencli/spotify.env file, reads OAuth tokens from ~/.opencli/spotify-tokens.json, and sends these secrets as HTTP Basic credentials in token refresh and exchange requests to accounts.spotify.com.
clientSecret: processEnv.SPOTIFY_CLIENT_SECRET || fileEnv.SPOTIFY_CLIENT_SECRET || '',
vector:code
Manipulation
mediumContextual riskmedium confidenceThe shipped twitter reply-dm adapter sends one identical user-supplied text into up to max (default 20) recent DM conversations in a single invocation (scroll-loading conversations, typing into dmComposerTextInput, clicking send), giving the agent a bulk-messaging capability suited to spam or phishing at scale from the logged-in account, illustrated by its own example text promoting a WeChat contact.
{ name: 'max', type: 'int', required: false, default: 20, help: 'Maximum number of conversations to reply to (default: 20)' },vector:code
Counter-argument considered
The shipped reply-dm adapter is reachable reference code with a concrete source→sink chain (kwargs.text → composer insertText → send click) looping over up to 20 recent DM conversations; bulk same-text DM sending is not a listed carve-out, no mitigation neutralizes it (skip-replied only dedupes), and no declared claim covers an operational bulk-messaging tool.
Supply chain
mediumContextual riskhigh confidenceThe twitter shared adapter resolves its GraphQL queryId at runtime by fetching a GitHub-hosted placeholder.json (fa0311/twitter-openapi) ahead of an offline bundle scan, so a mutable remote source can feed the identifier embedded into x.com API request URLs (bounded by sanitizeQueryId's QUERY_ID_PATTERN check, and the sink remains the declared x.com service).
Order: GitHub placeholder.json FIRST (more reliable — fa0311/twitter-openapi
vector:code
Counter-argument considered
The runtime resolution path fetches placeholder.json from the mutable refs/heads/main branch of third-party repo fa0311/twitter-openapi ahead of the bundle fallback and feeds the resolved queryId into x.com GraphQL request URLs; QUERY_ID_PATTERN in sanitizeQueryId (SRC:1788db4e23d2931d) and the x.com-only sink bound the impact but do not make the unpinned remote dependency a carve-out or a declared behavior.