What happened?
qwen channel start <channel-name> for a DingTalk channel prints credentials to stdout in plaintext on every connect:
- The fully resolved SDK client config object — including
clientId and clientSecret — in Node's util.inspect format.
- 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:
- Configure a
dingtalk channel with any clientId / clientSecret pair.
- Start it detached with redirection:
setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &
- Run
grep -i clientSecret /tmp/channel.log — the secret is present in plaintext, together with a res.data line carrying the stream ticket.
- 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:
- 完整解析后的 SDK client 配置对象 —— 含
clientId 和 clientSecret ——,以 Node util.inspect 格式输出。
- 一行
res.data {...},携带网关 endpoint 和 ticket。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.4 的 dist/client.mjs,getEndpoint() 内部:
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.write 加 sanitizeLogText —— 并且已经消掉了 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/logging 和 type/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 开关。
无论选哪种,都应覆盖重连路径(DingtalkConnectionManager 的 createClient),并补一个回归测试,断言 clientSecret 和 ticket 都不可能出现在 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 条目通过 ${...} 插值引用。此处不复现任何密钥值。
其他需要知道的信息
复现步骤,不需要真实凭据即可验证:
- 用任意一对
clientId / clientSecret 配置一个 dingtalk channel。
- 脱离终端并重定向启动:
setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &
- 执行
grep -i clientSecret /tmp/channel.log —— 密钥明文可见,同时还有携带 stream ticket 的 res.data 行。
- 触发一次重连 —— 同样的输出会再次出现。
已经这样运行 DingTalk channel 的用户,应把现有日志文件视为含有有效凭据:轮换 DingTalk 应用密钥,并清空或删除该日志。
What happened?
qwen channel start <channel-name>for a DingTalk channel prints credentials to stdout in plaintext on every connect:clientIdandclientSecret— in Node'sutil.inspectformat.res.data {...}line carrying the gatewayendpointandticket. 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:
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
clientSecretand a valid streamticketsitting 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, insidegetEndpoint():console.log(this.config)— unconditionalconsole.log("res.data", JSON.stringify(res.data))— unconditionalNeither call is gated by the SDK's
debugflag or routed through itsprintDebug()helper. The same two statements exist indist/client.cjs.2.0.4is 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.writeplussanitizeLogText— and it already neutralizes one of the SDK's noisy logs:installStructuredDownstreamHandler()inpackages/channels/dingtalk/src/DingtalkAdapter.tssetsclient.debug = falseand overridesclient.onDownStream, with the in-code comment "Keep raw SDK downstream frames off stdout". That override removes the SDK'sconsole.log(data)from the downstream path.getEndpoint()is a different path and is not covered by it, andcreateClient()then hands the real secret to the very object that gets printed:new DWClient({ clientId: ..., clientSecret: ... }).Still present on
mainas of 2026-09-03:packages/channels/dingtalk/package.jsondeclares"dingtalk-stream-sdk-nodejs": "^2.0.4", and the adapter still overrides onlyonDownStream.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/loggingandtype/enhancement), not secret exposure. PR #5998 is precisely theonDownStreamoverride described above. ThegetEndpoint()config and ticket leak is a separate code path that #5998 never touched.What did you expect to happen?
Neither
clientSecretnor the streamticket(nor the intermediateaccess_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:
console.logonly for the narrow window around the SDK connect /getEndpointcall, then re-emit a redacted summary.getEndpointon the client instance, the same wayonDownStreamis already overridden.console.logcalls behind the existingdebugflag.Whichever is chosen should also cover the reconnect path (
DingtalkConnectionManager'screateClient), and a regression test should assert that neitherclientSecretnorticketcan appear on stdout or stderr.Client information
Client Information
Deliberately not a full
/aboutpaste: 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 theenvblock 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:
dingtalkchannel with anyclientId/clientSecretpair.setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &grep -i clientSecret /tmp/channel.log— the secret is present in plaintext, together with ares.dataline carrying the stream ticket.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:clientId和clientSecret——,以 Nodeutil.inspect格式输出。res.data {...},携带网关endpoint和ticket。ticket 是有效的连接凭据:SDK 用它拼出流地址${endpoint}?ticket=${ticket}。输出形态如下,所有敏感值已替换为占位符:
channel 服务通常是脱离终端启动、并把 stdout 重定向到日志文件的,这也正是问题被发现的场景:一个长期运行的部署,其 channel 日志里就躺着
clientSecret和一个有效的 streamticket。由于连接管理器在重连时会重建 client,这段输出每次重连都会重复打印,日志里的凭据会不断累积。根因在依赖里,不在 qwen-code 自己的日志代码里。
dingtalk-stream-sdk-nodejs@2.0.4的dist/client.mjs,getEndpoint()内部: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.write加sanitizeLogText—— 并且已经消掉了 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/logging和type/enhancement),而不是密钥暴露。PR #5998 正是上文提到的那个onDownStream覆盖。getEndpoint()的配置与 ticket 泄漏是另一条代码路径,#5998 从未涉及。期望的行为
无论 SDK 内部的调试行为如何,
clientSecret、streamticket(以及中间的access_token)都不应该出现在 stdout 或 stderr。建连时最多输出一条脱敏摘要 —— 例如[DingTalk:<channel-name>] connecting (clientId=dingxxxx…)——,与 adapter 自身日志已有的脱敏方式保持一致。可选的修复方向,仅作建议而非规定:
getEndpoint调用的狭窄窗口内拦截console.log,之后重新输出一条脱敏摘要。onDownStream那样,在 client 实例上覆盖getEndpoint。console.log纳入已有的debug开关。无论选哪种,都应覆盖重连路径(
DingtalkConnectionManager的createClient),并补一个回归测试,断言clientSecret和ticket都不可能出现在 stdout 或 stderr。客户端信息
Client Information
这里刻意没有粘贴完整的
/about输出:本报告主题就是凭据泄漏,因此主机名、用户路径和账号标识一律有意省略。登录信息
模型认证与本问题无关。DingTalk channel 本身在
settings.json中配置:凭据放在env段,channel 条目通过${...}插值引用。此处不复现任何密钥值。其他需要知道的信息
复现步骤,不需要真实凭据即可验证:
clientId/clientSecret配置一个dingtalkchannel。setsid nohup qwen channel start <channel-name> >> /tmp/channel.log 2>&1 < /dev/null &grep -i clientSecret /tmp/channel.log—— 密钥明文可见,同时还有携带 stream ticket 的res.data行。已经这样运行 DingTalk channel 的用户,应把现有日志文件视为含有有效凭据:轮换 DingTalk 应用密钥,并清空或删除该日志。