Skip to content

fix: address security review - SSRF protection, credential logging, memory leak, type safety#2

Merged
AkkiaS7 merged 10 commits intodevfrom
copilot/sub-pr-1
Feb 13, 2026
Merged

fix: address security review - SSRF protection, credential logging, memory leak, type safety#2
AkkiaS7 merged 10 commits intodevfrom
copilot/sub-pr-1

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 13, 2026

Addresses security and code quality issues from PR #1 code review: webhook URL credential leakage in logs, SSRF vulnerability via unrestricted webhook POST, memory leak in pending request cleanup, and loose type safety on target_mode parameter.

Security Fixes

  • SSRF Protection with Flexible Configuration:

    • Add FEISHU_WEBHOOK_ALLOWED_HOSTS config using JSON format (e.g., ["open.feishu.cn","open.larksuite.com"])
    • Not set: Uses default whitelist ["open.feishu.cn", "open.larksuite.com"]
    • Set to []: Disables whitelist (allows any domain - use with caution)
    • Set to custom list: Uses specified whitelist
    • Domain validation uses hostname (not netloc), allowing any port on whitelisted domains
  • Enhanced Credential Logging Protection:

    • Extract mask_url() utility that validates URLs have proper scheme and hostname
    • Uses parsed.hostname instead of parsed.netloc to avoid leaking userinfo, port info
    • Returns [url_masked] for non-standard URLs
    • Masks previous_webhook in success logs to prevent credential leakage
  • Memory Leak Fix: Add _cleanup_pending_webhook_requests() calls to handle_bind(), handle_bind_mode_callback(), and handle_message() to prevent unbounded dictionary growth from abandoned flows.

Code Quality

  • Type Safety: Change target_mode from str to Literal["bot", "webhook"] in TargetPlatformSender protocol and FeishuSender implementation for compile-time validation.

  • Test Coverage:

    • Add tests for webhook TTL cleanup and domain whitelist enforcement (SSRF protection)
    • Add tests for hostname-based validation with custom port support
    • Add tests for disabled whitelist mode
  • DRY: Extract URL masking constants and logic to shared utils/url_masking.py module.

  • Configuration: Add get_webhook_allowed_hosts() method to Settings to handle default/custom/disabled whitelist logic cleanly.

# Before: webhook URL with token logged in plaintext
logger.debug("target=%s", target)  # logs full webhook URL with secret token

# After: token sanitized using hostname only
safe_target = mask_url(target)  # https://open.feishu.cn/open-apis/bot/v2...k/abc123
logger.debug("target=%s", safe_target)

Configuration Examples

# Use default whitelist (recommended)
FEISHU_WEBHOOK_ALLOWED_HOSTS=

# Disable whitelist (allows any domain - use with caution)
FEISHU_WEBHOOK_ALLOWED_HOSTS=[]

# Custom whitelist with JSON format
FEISHU_WEBHOOK_ALLOWED_HOSTS=["open.feishu.cn","custom.domain.com"]

Security Validation: CodeQL analysis passes with 0 alerts.


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 6 commits February 13, 2026 11:36
…tection, memory leak, and type safety

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
…ogging, fix test naming

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
…uplication

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
… messages

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 13, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • astral.sh
    • Triggering command: /usr/bin/curl curl -LsSf REDACTED (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] Add Feishu webhook binding support in Telegram bot fix: address security review - SSRF protection, credential logging, memory leak, type safety Feb 13, 2026
Copilot AI requested a review from AkkiaS7 February 13, 2026 11:45
@AkkiaS7 AkkiaS7 marked this pull request as ready for review February 13, 2026 11:59
Copilot AI review requested due to automatic review settings February 13, 2026 11:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

本 PR 针对 StickerHub 的飞书 webhook 绑定链路,修复安全审计与代码质量问题(SSRF 防护、日志凭据泄露、pending 请求清理、类型收紧),并补充相应测试与文档/配置示例,降低 webhook 绑定的安全与运维风险。

Changes:

  • 新增 webhook 域名白名单配置并在 webhook URL 归一化时做校验,降低 SSRF 风险
  • 抽取 URL 脱敏工具 mask_url(),避免日志中输出 webhook token
  • 增加 pending webhook 绑定请求的清理逻辑与测试;收紧 target_mode 类型到 Literal["bot", "webhook"]

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/stickerhub/services/binding.py 注入 webhook allowed hosts,URL 归一化增加白名单校验,并在失败日志中脱敏输出
src/stickerhub/utils/url_masking.py 新增通用 URL 脱敏工具供日志使用
src/stickerhub/config.py 新增 FEISHU_WEBHOOK_ALLOWED_HOSTS 配置项映射到 Settings
src/stickerhub/main.py 将 Settings 中的 allowed hosts 传入 BindingService
src/stickerhub/core/ports.py TargetPlatformSender.send()target_mode 改为 Literal 收紧类型
src/stickerhub/adapters/telegram_source.py 在多个入口调用 pending webhook 请求清理,避免字典无界增长
src/stickerhub/adapters/feishu_sender.py 发送前对 webhook target 做脱敏日志输出
tests/test_telegram_source.py 增加 webhook pending 清理的单测
tests/test_binding_sqlite.py 增加 webhook 域名白名单(SSRF 防护)相关单测
README.md / .env.example 记录新增配置项及示例

Comment on lines +19 to +25
if parsed.path and len(parsed.path) > PATH_MASK_THRESHOLD:
masked_path = (
f"{parsed.path[:PATH_PREFIX_LENGTH]}...{parsed.path[-PATH_SUFFIX_LENGTH:]}"
)
else:
masked_path = parsed.path
return f"{parsed.scheme}://{parsed.netloc}{masked_path}"
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mask_url() 目前对很多“非标准 URL”输入不会真正脱敏:urlparse() 很少抛异常,像 "token_only" 这类字符串会被格式化成 ://token_only 原样写入日志;另外使用 parsed.netloc 可能把 user:pass@host 这类 userinfo 直接打到日志里。建议在拼接返回值前显式校验 scheme/hostname,并使用 parsed.hostname(且忽略/剔除 userinfo、端口、query/fragment),否则直接返回固定占位符。

Suggested change
if parsed.path and len(parsed.path) > PATH_MASK_THRESHOLD:
masked_path = (
f"{parsed.path[:PATH_PREFIX_LENGTH]}...{parsed.path[-PATH_SUFFIX_LENGTH:]}"
)
else:
masked_path = parsed.path
return f"{parsed.scheme}://{parsed.netloc}{masked_path}"
# 仅在解析结果有明确的协议和主机名时才返回拼接后的 URL
if not parsed.scheme or not parsed.hostname:
return "[url_masked]"
if parsed.path and len(parsed.path) > PATH_MASK_THRESHOLD:
masked_path = (
f"{parsed.path[:PATH_PREFIX_LENGTH]}...{parsed.path[-PATH_SUFFIX_LENGTH:]}"
)
else:
masked_path = parsed.path
# 仅使用 hostname,避免将 userinfo、端口等敏感信息写入日志
return f"{parsed.scheme}://{parsed.hostname}{masked_path}"

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复,commit 7851551fb62662

现在 mask_url() 会:

  1. 验证 URL 有明确的 scheme 和 hostname,否则返回 [url_masked]
  2. 仅使用 parsed.hostname,避免 userinfo、端口等敏感信息泄露
  3. 对非标准 URL(如 "token_only")返回固定占位符而非原样输出

Comment on lines 24 to 28
feishu_webhook_allowed_hosts: list[str] = Field(
default=["open.feishu.cn", "open.larksuite.com"],
alias="FEISHU_WEBHOOK_ALLOWED_HOSTS",
)
log_level: str = Field(default="INFO", alias="LOG_LEVEL")
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FEISHU_WEBHOOK_ALLOWED_HOSTS 在 README/.env.example 里说明为“逗号分隔、留空用默认值”,但这里字段类型是 list[str]:从 .env 读到 FEISHU_WEBHOOK_ALLOWED_HOSTS=(空字符串)或 a,b(非 JSON)时,pydantic-settings 通常会解析失败并导致启动报错。建议在 SettingsConfigDict 配置 env_ignore_empty=True 并设置 env_parse_delimiter=","(或将该字段改成 str | None 后自行 split+strip)。

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复,commit 7851551

现在使用 JSON 格式解析 FEISHU_WEBHOOK_ALLOWED_HOSTS

  • 不设置:使用默认白名单 ["open.feishu.cn", "open.larksuite.com"]
  • 设为 []:禁用白名单校验
  • 设为 JSON 数组:如 ["open.feishu.cn","custom.domain.com"]

示例配置:

# 使用默认白名单
FEISHU_WEBHOOK_ALLOWED_HOSTS=

# 禁用白名单(谨慎使用)
FEISHU_WEBHOOK_ALLOWED_HOSTS=[]

# 自定义白名单
FEISHU_WEBHOOK_ALLOWED_HOSTS=["open.feishu.cn","custom.domain.com"]

Comment on lines +520 to +526
if not parsed.netloc:
return None

# 域名白名单校验(SSRF 防护)
if parsed.netloc.lower() not in [host.lower() for host in allowed_hosts]:
return None

Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

域名白名单校验使用了 parsed.netloc 做精确匹配;netloc 会包含端口(如 open.feishu.cn:443),也可能包含 userinfo,从而导致合法 URL 被误拒或校验语义不清。建议改用 parsed.hostname 做白名单匹配,并根据需要显式允许/禁止端口(例如仅允许空端口或 443)。

Suggested change
if not parsed.netloc:
return None
# 域名白名单校验(SSRF 防护)
if parsed.netloc.lower() not in [host.lower() for host in allowed_hosts]:
return None
# 必须有合法主机名
if not parsed.hostname:
return None
# 仅允许默认端口或 443,避免通过非常规端口绕过校验
if parsed.port not in (None, 443):
return None
# 域名白名单校验(SSRF 防护)——仅基于 hostname,而非包含端口的 netloc
hostname = parsed.hostname.lower()
if hostname not in [host.lower() for host in allowed_hosts]:
return None

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复,commit 7851551fb62662

现在域名白名单校验:

  1. 使用 parsed.hostname 而非 parsed.netloc 进行匹配
  2. 不限制端口(任何端口都可以通过校验)
  3. 已添加测试验证带自定义端口的 URL(如 https://open.feishu.cn:8443/...)可以正常通过

Comment on lines +421 to +429
normalized_url = _normalize_feishu_webhook_url(webhook_url, self._webhook_allowed_hosts)
if not normalized_url:
# 脱敏 URL 用于日志
masked_url = mask_url(webhook_url)
logger.warning(
"Webhook 绑定失败: 平台=%s user=%s 原因=URL格式不合法",
"Webhook 绑定失败: 平台=%s user=%s 原因=URL格式不合法或域名不在白名单内 url=%s",
source_platform,
source_user_id,
masked_url,
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handle_bind_webhook() 虽然在失败日志里对输入 URL 做了脱敏,但后续成功日志仍会输出 details.get("previous_webhook")(旧 webhook URL),这通常包含敏感 token,依然会造成凭据泄露。建议对 previous_webhook 同样使用 mask_url() 后再记录,或只记录是否存在/哈希摘要而不记录 URL 本身。

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复,commit 7851551

现在成功日志中的 previous_webhook 会先使用 mask_url() 脱敏后再记录,防止旧凭据泄露:

previous_webhook_masked = (
    mask_url(details["previous_webhook"])
    if details.get("previous_webhook")
    else None
)
logger.info("... previous_webhook=%s ...", previous_webhook_masked)

@AkkiaS7
Copy link
Copy Markdown
Contributor

AkkiaS7 commented Feb 13, 2026

@copilot apply changes based on the comments in this thread

  1. 只按照hotname验证白名单 不要限制端口,额外提供关闭白名单验证的配置
  2. env域名白名单用json List[str]的形式

Copilot AI and others added 2 commits February 13, 2026 12:14
…option to disable whitelist, use JSON format for env config

Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
Co-authored-by: AkkiaS7 <68485070+AkkiaS7@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Feb 13, 2026

@copilot apply changes based on the comments in this thread

  1. 只按照hotname验证白名单 不要限制端口,额...

已完成所有修改,commits 7851551fb62662

主要变更

  1. 域名白名单仅验证 hostname,不限制端口

    • 使用 parsed.hostname 替代 parsed.netloc
    • 任何端口都可通过验证
    • 已添加测试验证自定义端口支持
  2. 提供关闭白名单验证的配置

    • 不设置:使用默认白名单 ["open.feishu.cn", "open.larksuite.com"]
    • 设为 []:完全禁用白名单校验
    • 设为自定义列表:使用指定白名单
  3. 使用 JSON 格式配置域名白名单

    # .env 文件示例
    FEISHU_WEBHOOK_ALLOWED_HOSTS=["open.feishu.cn","custom.domain.com"]
    # 或禁用白名单
    FEISHU_WEBHOOK_ALLOWED_HOSTS=[]
  4. 其他安全改进

    • URL 脱敏使用 hostname 避免泄露 userinfo/端口
    • 成功日志中的 previous_webhook 也进行脱敏
    • 对非标准 URL 返回固定占位符

安全验证:CodeQL 分析通过,0 个告警

@AkkiaS7 AkkiaS7 merged commit 433000e into dev Feb 13, 2026
1 check passed
@AkkiaS7 AkkiaS7 deleted the copilot/sub-pr-1 branch February 13, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants