☰
DiscordChatExporter 应用安全实践:5 层防线上线前自查清单
2026/11/4 1:14:53 网站建设 项目流程

DiscordChatExporter 应用安全实践:5 层防线上线前自查清单

【免费下载链接】DiscordChatExporterSaves Discord chat logs to a file项目地址: https://gitcode.com/gh_mirrors/di/DiscordChatExporter

一个反直觉的问题

用 C# 写个 Discord 聊天记录导出器,会出安全问题吗?大多数新手会摇头。但你只要把视角换成"用户交给你一把能读他全部私聊的钥匙",答案立刻反转——这把钥匙怎么存、怎么验、怎么用、用完怎么收,每一步都是攻击面。

这篇 DiscordChatExporter 安全实践指南,按"边界 → 身份 → 数据 → 文件 → 运行时"5 层防线带你过一遍仓库真实代码。读完你能拿走的是一份上线前自查清单,不是一堆口号。

威胁速览

威胁可能后果对应章节
空/畸形 token 进入客户端鉴权绕过、异常吞错身份层
token 明文写进 settings 文件用户隐私泄露数据层
附件 URL 指向任意外域下载恶意文件文件层
忽视 Discord 限流账号被风控运行时层

第一层 · 边界:先把输入洗干净

风险场景:用户在 GUI 顶部那个 Token 框里粘贴了带引号或前后空格的 token——Discord 客户端复制时经常带这俩东西。后果:直接把脏串塞进 Authorization 头,要么鉴权失败要么触发 Discord 的 4xx 风暴。

仓库的处理在DashboardViewModel.PullGuildsAsync里只有一行,但方向对了:

var token = Token?.Trim('"', ' '); if (string.IsNullOrWhiteSpace(token)) return;

紧接着new DiscordClient(token, ...)把干净串交给客户端。而客户端在发请求前又走了一步——DiscordClient用TryAddWithoutValidation写 Authorization 头,注释里明确说明"token 可能含特殊字符",这是绕开 HttpClient 默认校验的刻意为之。

动作:在你自己的应用里,凡是来自用户界面的凭据,进入业务前必须过一道 trim + 非空检查,再交给下游。

第二层 · 身份:先验明正身,再放行

风险场景:用户拿一个 user token 进来,你直接当 bot 用;或者 bot 没开 MESSAGE_CONTENT intent,你闷头拉消息拉出一堆空壳。后果:错误提示误导用户,bot 侧静默失败。

DiscordClient的ResolveTokenKindAsync会先用 user 身份打一次/users/@me,失败再换Bot {token}前缀重试,两次都 401 就抛Authentication token is invalid.。而 bot 走/applications/@me检查IsMessageContentIntentEnabled,没开就明确报 "missing the MESSAGE_CONTENT privileged intent"——不是笼统的"拉不到消息"。

动作:写身份探测时,把每一种失败都映射到一句人能看懂的错,别让"拉不到数据"背锅。

第三层 · 数据:token 落盘必须加密

风险场景:IsTokenPersisted默认开启,token 会写进用户目录的 settings JSON 文件。如果这里存明文,任何同机进程或备份工具都能读到。

仓库的答案在SettingsService.TokenEncryptionConverter:密钥用 PBKDF2(SHA-256,60 万轮)从"机器 ID + 项目盐"派生,数据用 AES-GCM 加密,nonce 用RandomNumberGenerator.Fill生成,落盘格式是enc:前缀加 hex。旧版本明文读取时直接透传,下次Save()时自动加密——这是很干净的向前兼容。

using var aes = new AesGcm(Key.Value, 16); aes.Decrypt( encryptedData.AsSpan(0, 12), encryptedData.AsSpan(28), encryptedData.AsSpan(12, 16), tokenData );

另外SettingsService.Save()里有段巧思:IsTokenPersisted为 false 时,Save()前临时把LastToken置空,落盘后再恢复,这样"只记一次"的语义不会被 JSON 序列化破坏。

动作:任何会持久化的密钥,先问自己"这个文件被拷走会怎样",答不上就加密。

第四层 · 文件:下载只信白名单 host

风险场景:附件 URL 来自 Discord 消息,理论上可以是任意 host。如果你不做限制,恶意消息里的http://evil.example/a.exe就会被下载到本地目录。

ExportAssetDownloader.NormalizeUrl里有一段明确的 host 白名单:

if ( !string.Equals(uri.Host, "cdn.discordapp.com", StringComparison.OrdinalIgnoreCase) && !string.Equals(uri.Host, "media.discordapp.net", StringComparison.OrdinalIgnoreCase) ) { return url; }

只有这两个 host 才会走"去掉签名参数再规范化"的逻辑,其他一律原样处理、不下载。文件命名兜底用 URL 的 hash,避免同名覆盖。

动作:任何"从远端拉文件"的代码路径,都该有一个 host 白名单作为第一道闸。

第五层 · 运行时:限流和重试要可配置

风险场景:用户开了并行、又选了IgnoreAll,Discord 开始回 429,你的程序不停冲,最终账号被风控。

仓库把它做成用户显式决策——RateLimitPreference是个[Flags]枚举,RespectAll是默认值,RespectForUserTokens / RespectForBotTokens可分别开关。命中X-RateLimit-Remaining <= 0时按X-RateLimit-Reset-After等待,加 1 秒 buffer 并用Clamp(0, 60s)兜住 Discord 偶尔返回的离谱值。底层再叠一层 Polly 指数退避,Retry-After存在就加 1 秒用,不存在就2^n + 1。

var delay = (resetAfterDelay.Value + TimeSpan.FromSeconds(1)) .Clamp(TimeSpan.Zero, TimeSpan.FromSeconds(60)); await Task.Delay(delay, innerCancellationToken);

动作:把"要不要守规矩"做成用户可见的开关,同时给默认值留一个安全档。

上线前 10 分钟自查清单

  • 用户输入的凭据在进业务前做了 trim + 非空校验
  • 凭据持久化路径使用了加密,且保留了旧明文版本的兼容读取
  • "记住凭据"开关关闭时,Save()真的没把凭据写进文件
  • 所有远端下载走 host 白名单,非白名单 URL 不落盘
  • 429 处理里有Retry-After分支,且上限被 clamp 到合理值
  • 限流策略是显式可配的,默认档偏向保守
  • 每一种鉴权失败都能映射到一句人话错误,而不是笼统的"请求失败"

持续维护

上游 Discord API 的限流头、CDN host 都在变。每次发版前把Http.cs的重试参数和ExportAssetDownloader的 host 白名单对一遍当前文档,跑一次DiscordChatExporter.Cli.Tests里的SelfContainedSpecs确认加密路径没回归。发现白名单或重试参数落后于上游,就在同一个 PR 里改代码和注释,别留 TODO。

延伸阅读:DiscordClient.cs、TokenEncryptionConverter

【免费下载链接】DiscordChatExporterSaves Discord chat logs to a file项目地址: https://gitcode.com/gh_mirrors/di/DiscordChatExporter

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询