Skip to content

DingTalk channel prints clientSecret and stream ticket to stdout on every connect #10936

Description

@yiliang114

What happened?

qwen channel start <channel-name> for a DingTalk channel prints credentials to stdout in plaintext on every connect:

  1. The fully resolved SDK client config object — including clientId and clientSecret — in Node's util.inspect format.
  2. A res.data {...} line carrying the gateway endpoint and ticket. The ticket is a live connection credential: the SDK composes the stream URL as ${endpoint}?ticket=${ticket}.

Shape of the output, with every sensitive value replaced by a placeholder:

{
  clientId: 'dingxxxxxxxxxxxxxxxx',
  clientSecret: '<REDACTED-CLIENT-SECRET>',
  keepAlive: false,
  subscriptions: [ ... ]
}
res.data {"endpoint":"wss://wss-open-connection.dingtalk.com:443/connect","ticket":"<REDACTED-TICKET>"}
[DingTalk:<channel-name>] Connected via stream.

Channel services are normally started detached with stdout redirected into a log file, and that is how this was noticed: a long-running deployment had the clientSecret and a valid stream ticket sitting in its channel log. Because the connection manager recreates the client on reconnect, the dump repeats on every reconnect, so the log keeps accumulating credentials over time.

The root cause is in a dependency, not in qwen-code's own logging. In dingtalk-stream-sdk-nodejs@2.0.4, dist/client.mjs, inside getEndpoint():

  • console.log(this.config) — unconditional
  • console.log("res.data", JSON.stringify(res.data)) — unconditional

Neither call is gated by the SDK's debug flag or routed through its printDebug() helper. The same two statements exist in dist/client.cjs. 2.0.4 is also the latest version published on npm, so there is no upstream release to upgrade to.

qwen-code's own adapter logging is careful — everything goes through process.stderr.write plus sanitizeLogText — and it already neutralizes one of the SDK's noisy logs: installStructuredDownstreamHandler() in packages/channels/dingtalk/src/DingtalkAdapter.ts sets client.debug = false and overrides client.onDownStream, with the in-code comment "Keep raw SDK downstream frames off stdout". That override removes the SDK's console.log(data) from the downstream path. getEndpoint() is a different path and is not covered by it, and createClient() then hands the real secret to the very object that gets printed: new DWClient({ clientId: ..., clientSecret: ... }).

Still present on main as of 2026-09-03: packages/channels/dingtalk/package.json declares "dingtalk-stream-sdk-nodejs": "^2.0.4", and the adapter still overrides only onDownStream.

Relation to #5997 / #5998 — not a duplicate. Issue #5997 "DingTalk stream logs print raw Buffer payloads" and its fix PR #5998 "fix(channels): structure DingTalk stream logs" targeted the downstream payload log, and the motivation there was operator readability (its labels include scope/logging and type/enhancement), not secret exposure. PR #5998 is precisely the onDownStream override described above. The getEndpoint() config and ticket leak is a separate code path that #5998 never touched.

What did you expect to happen?

Neither clientSecret nor the stream ticket (nor the intermediate access_token) should ever reach stdout or stderr, whatever the SDK does internally for debugging. A connect should emit at most a redacted summary — for example [DingTalk:<channel-name>] connecting (clientId=dingxxxx…) — consistent with how the adapter already sanitizes everything it logs itself.

Possible directions, offered as options rather than a prescription:

  • Intercept console.log only for the narrow window around the SDK connect / getEndpoint call, then re-emit a redacted summary.
  • Or override getEndpoint on the client instance, the same way onDownStream is already overridden.
  • Or avoid handing a printable secret to the SDK config at all, materializing it only when the request is built.
  • And/or ask upstream to gate those two console.log calls behind the existing debug flag.

Whichever is chosen should also cover the reconnect path (DingtalkConnectionManager's createClient), and a regression test should assert that neither clientSecret nor ticket can appear on stdout or stderr.

Client information

Client Information
Qwen Code: 0.23.0
Platform:  Linux x64
Node:      v24.19.0
Install:   npm global (npm install -g @qwen-code/qwen-code@latest)
Channel:   dingtalk (dingtalk-stream-sdk-nodejs 2.0.4)

Deliberately not a full /about paste: this report is about credential leakage, so hostnames, user paths, and account identifiers are omitted on purpose.

Login information

Model authentication is unrelated to this bug. The DingTalk channel itself is configured in settings.json: the credentials live in the env block and are referenced from the channel entry through ${...} interpolation. No secret values are reproduced here.

Anything else we need to know?

Reproduction, which needs no real credentials to demonstrate:

  1. Configure a dingtalk channel with any clientId / clientSecret pair.
  2. Start it detached with redirection: setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &
  3. Run grep -i clientSecret /tmp/channel.log — the secret is present in plaintext, together with a res.data line carrying the stream ticket.
  4. Force a reconnect — the dump repeats.

Anyone already running a DingTalk channel this way should treat the existing log file as holding live credentials: rotate the DingTalk app secret and truncate or remove the log.

中文

发生了什么?

DingTalk channel 执行 qwen channel start <channel-name> 时,每次建连都会把凭据明文打到 stdout:

  1. 完整解析后的 SDK client 配置对象 —— 含 clientIdclientSecret ——,以 Node util.inspect 格式输出。
  2. 一行 res.data {...},携带网关 endpointticket。ticket 是有效的连接凭据:SDK 用它拼出流地址 ${endpoint}?ticket=${ticket}

输出形态如下,所有敏感值已替换为占位符:

{
  clientId: 'dingxxxxxxxxxxxxxxxx',
  clientSecret: '<REDACTED-CLIENT-SECRET>',
  keepAlive: false,
  subscriptions: [ ... ]
}
res.data {"endpoint":"wss://wss-open-connection.dingtalk.com:443/connect","ticket":"<REDACTED-TICKET>"}
[DingTalk:<channel-name>] Connected via stream.

channel 服务通常是脱离终端启动、并把 stdout 重定向到日志文件的,这也正是问题被发现的场景:一个长期运行的部署,其 channel 日志里就躺着 clientSecret 和一个有效的 stream ticket。由于连接管理器在重连时会重建 client,这段输出每次重连都会重复打印,日志里的凭据会不断累积。

根因在依赖里,不在 qwen-code 自己的日志代码里。 dingtalk-stream-sdk-nodejs@2.0.4dist/client.mjsgetEndpoint() 内部:

  • console.log(this.config) —— 无条件执行
  • console.log("res.data", JSON.stringify(res.data)) —— 无条件执行

两处都不受 SDK 的 debug 开关约束,也没有走它的 printDebug()dist/client.cjs 里存在同样两条语句。而 2.0.4 已经是 npm 上发布的最新版本,所以没有可用的上游版本可升级。

qwen-code 自己的 adapter 日志是克制的 —— 全部走 process.stderr.writesanitizeLogText —— 并且已经消掉了 SDK 的一处噪音日志:packages/channels/dingtalk/src/DingtalkAdapter.ts 里的 installStructuredDownstreamHandler() 设置了 client.debug = false 并覆盖了 client.onDownStream,代码注释写的是 "Keep raw SDK downstream frames off stdout"。这个覆盖去掉了下行链路里的 console.log(data)。但 getEndpoint() 是另一条路径,不在覆盖范围内;而 createClient() 恰恰把真实密钥交给了那个会被打印出来的对象:new DWClient({ clientId: ..., clientSecret: ... })

截至 2026-09-03,main 上仍然存在:packages/channels/dingtalk/package.json 声明 "dingtalk-stream-sdk-nodejs": "^2.0.4",adapter 依然只覆盖了 onDownStream

#5997 / #5998 的关系 —— 不是重复 issue。 issue #5997 "DingTalk stream logs print raw Buffer payloads" 及其修复 PR #5998 "fix(channels): structure DingTalk stream logs" 针对的是下行 payload 日志,当时的出发点是运维可读性(label 含 scope/loggingtype/enhancement),而不是密钥暴露。PR #5998 正是上文提到的那个 onDownStream 覆盖。getEndpoint() 的配置与 ticket 泄漏是另一条代码路径,#5998 从未涉及。

期望的行为

无论 SDK 内部的调试行为如何,clientSecret、stream ticket(以及中间的 access_token)都不应该出现在 stdout 或 stderr。建连时最多输出一条脱敏摘要 —— 例如 [DingTalk:<channel-name>] connecting (clientId=dingxxxx…) ——,与 adapter 自身日志已有的脱敏方式保持一致。

可选的修复方向,仅作建议而非规定:

  • 只在 SDK connect / getEndpoint 调用的狭窄窗口内拦截 console.log,之后重新输出一条脱敏摘要。
  • 或者像已经覆盖 onDownStream 那样,在 client 实例上覆盖 getEndpoint
  • 或者根本不把可打印的密钥交给 SDK 配置对象,改为在构造请求时才取用。
  • 以及/或者推动上游把那两条 console.log 纳入已有的 debug 开关。

无论选哪种,都应覆盖重连路径(DingtalkConnectionManagercreateClient),并补一个回归测试,断言 clientSecretticket 都不可能出现在 stdout 或 stderr。

客户端信息

Client Information
Qwen Code: 0.23.0
Platform:  Linux x64
Node:      v24.19.0
Install:   npm global (npm install -g @qwen-code/qwen-code@latest)
Channel:   dingtalk (dingtalk-stream-sdk-nodejs 2.0.4)

这里刻意没有粘贴完整的 /about 输出:本报告主题就是凭据泄漏,因此主机名、用户路径和账号标识一律有意省略。

登录信息

模型认证与本问题无关。DingTalk channel 本身在 settings.json 中配置:凭据放在 env 段,channel 条目通过 ${...} 插值引用。此处不复现任何密钥值。

其他需要知道的信息

复现步骤,不需要真实凭据即可验证:

  1. 用任意一对 clientId / clientSecret 配置一个 dingtalk channel。
  2. 脱离终端并重定向启动:setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &
  3. 执行 grep -i clientSecret /tmp/channel.log —— 密钥明文可见,同时还有携带 stream ticket 的 res.data 行。
  4. 触发一次重连 —— 同样的输出会再次出现。

已经这样运行 DingTalk channel 的用户,应把现有日志文件视为含有有效凭据:轮换 DingTalk 应用密钥,并清空或删除该日志。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions