Woahai321 ListSync SRF漏洞CVE-2026-3958
Woahai321 ListSync 是一个用于列表同步的开源工具,作为影视清单自动化同步中间件,可用于打通观影发现与媒体服务器请求环节。
一、基本情况
Woahai321 ListSync 是一款开源自动化工具,全自动化、多平台兼容、可视化管理、轻量易部署、开源可定制、适配多用户媒体中心。
可将 IMDb、Trakt、Letterboxd 的影视清单自动同步到 Overseerr/Jellyseerr 媒体请求服务器,实现观影清单与家庭媒体中心无缝对接。

栋科技漏洞库关注到 Woahai321 ListSync 在 0.6.6 之前的版本中存在的SSRF漏洞,漏洞追踪为CVE-2026-3958,CVSS 4.0评分5.3。
二、漏洞分析
CVE-2026-3958 源于 api_server.py 文件 JSON Handler 组件中,requests.post 函数在处理用户提供的输入时未进行有效的安全验证。
POST /api/notifications/test 接口在请求体中接收用户提供的 webhook_url,并将其直接传入 requests.post()(或 DiscordWebhook)。
这期间未进行任何 URL 校验或白名单检查,这就导致攻击者可以发送构造的 JSON 数据,将 webhook_url 指向自己控制的服务器。
远程攻击者通过构造恶意 JSON 请求,诱导服务器向内网或第三方服务器发起非预期的 POST 请求,导致服务端请求伪造(SSRF)。
应用会向该 URL 发起出站 HTTP 请求,可通过服务器 IP 产生的 DNS 回调记录确认攻击成功。
此类 SSRF 漏洞可用于内网扫描、窃取云平台元数据(如 AWS IMDSv1)或端口探测,探测内网服务、绕过防火墙或攻击内部应用。
1、来源
// list-sync-main/api_server.py#L6858C1-L6968C99
6858→async def test_discord_notification(payload: dict = None):
6859→ """Send a test Discord notification to verify webhook configuration"""
6860→ try:
6861→ # Get Discord webhook URL from request body or environment
6862→ webhook_url = None
6863→ if payload and 'webhook_url' in payload:
6864→ webhook_url = payload['webhook_url']
6865→
6866→ if not webhook_url:
6867→ webhook_url = os.getenv('DISCORD_WEBHOOK_URL', '')
6868→
6869→ if not webhook_url:
6870→ raise HTTPException(
6871→ status_code=400,
6872→ detail="Discord webhook URL is required. Please provide a webhook URL or set DISCORD_WEBHOOK_URL in your environment variables."
6873→ )
6874→
6875→ # Try to use the discord-webhook lipary if available
6876→ try:
6877→ from discord_webhook import DiscordWebhook, DiscordEmbed
6878→ from datetime import datetime
6879→
6880→ # Create webhook instance - explicitly set content to None to avoid duplicate messages
6881→ webhook = DiscordWebhook(url=webhook_url, username="ListSync Test", content=None)
6882→
6883→ # Create embed with test message
6884→ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
6885→ embed = DiscordEmbed(
6886→ title="🧪 Discord Integration Test",
6887→ description="If you see this message, Discord notifications are working correctly! ✅",
6888→ color=10181046 # Purple color
6889→ )
6890→
6891→ embed.add_embed_field(
6892→ name="Test Time",
6893→ value=current_time,
6894→ inline=True
6895→ )
6896→
6897→ embed.add_embed_field(
6898→ name="Status",
6899→ value="✅ Connected",
6900→ inline=True
6901→ )
6902→
6903→ embed.set_footer(text="ListSync Notification System")
6904→ embed.set_timestamp()
6905→
6906→ # Add embed to webhook (only embed, no content)
6907→ webhook.add_embed(embed)
6908→
6909→ # Send webhook
6910→ response = webhook.execute()
6911→
6912→ return {
6913→ "success": True,
6914→ "message": "Test notification sent successfully! Check your Discord channel.",
6915→ "timestamp": current_time
6916→ }
6917→
6918→ except ImportError:
6919→ # Fallback to using requests directly
6920→ from datetime import datetime
6921→ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
6922→
6923→ # Only send embed, no content to avoid duplicate messages
6924→ payload = {
6925→ "embeds": [{
6926→ "title": "🧪 Discord Integration Test",
6927→ "description": "If you see this message, Discord notifications are working correctly! ✅",
6928→ "color": 10181046,
6929→ "fields": [
6930→ {
6931→ "name": "Test Time",
6932→ "value": current_time,
6933→ "inline": True
6934→ },
6935→ {
6936→ "name": "Status",
6937→ "value": "✅ Connected",
6938→ "inline": True
6939→ }
6940→ ],
6941→ "footer": {
6942→ "text": "ListSync Notification System"
6943→ },
6944→ "timestamp": datetime.utcnow().isoformat()
6945→ }]
6946→ }
6947→
6948→ response = requests.post(webhook_url, json=payload, timeout=10)
6949→ response.raise_for_status()
6950→
6951→ return {
6952→ "success": True,
6953→ "message": "Test notification sent successfully! Check your Discord channel.",
6954→ "timestamp": current_time
6955→ }
6956→
6957→ except requests.exceptions.Timeout:
6958→ raise HTTPException(status_code=504, detail="Discord webhook request timed out")
6959→ except requests.exceptions.RequestException as e:
6960→ error_msg = f"Failed to send Discord notification: {str(e)}"
6961→ if hasattr(e, 'response') and e.response is not None:
6962→ error_msg += f" (Status: {e.response.status_code})"
6963→ raise HTTPException(status_code=500, detail=error_msg)
6964→ except Exception as e:
6965→ import traceback
6966→ error_detail = f"Failed to send test notification: {str(e)}\n{traceback.format_exc()}"
6967→ logging.error(error_detail)
6968→ raise HTTPException(status_code=500, detail=f"Failed to send test notification: {str(e)}")
2、接收点
// list-sync-main/api_server.py#L6858C1-L6968C99
6858→async def test_discord_notification(payload: dict = None):
6859→ """Send a test Discord notification to verify webhook configuration"""
6860→ try:
6861→ # Get Discord webhook URL from request body or environment
6862→ webhook_url = None
6863→ if payload and 'webhook_url' in payload:
6864→ webhook_url = payload['webhook_url']
6865→
6866→ if not webhook_url:
6867→ webhook_url = os.getenv('DISCORD_WEBHOOK_URL', '')
6868→
6869→ if not webhook_url:
6870→ raise HTTPException(
6871→ status_code=400,
6872→ detail="Discord webhook URL is required. Please provide a webhook URL or set DISCORD_WEBHOOK_URL in your environment variables."
6873→ )
6874→
6875→ # Try to use the discord-webhook lipary if available
6876→ try:
6877→ from discord_webhook import DiscordWebhook, DiscordEmbed
6878→ from datetime import datetime
6879→
6880→ # Create webhook instance - explicitly set content to None to avoid duplicate messages
6881→ webhook = DiscordWebhook(url=webhook_url, username="ListSync Test", content=None)
6882→
6883→ # Create embed with test message
6884→ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
6885→ embed = DiscordEmbed(
6886→ title="🧪 Discord Integration Test",
6887→ description="If you see this message, Discord notifications are working correctly! ✅",
6888→ color=10181046 # Purple color
6889→ )
6890→
6891→ embed.add_embed_field(
6892→ name="Test Time",
6893→ value=current_time,
6894→ inline=True
6895→ )
6896→
6897→ embed.add_embed_field(
6898→ name="Status",
6899→ value="✅ Connected",
6900→ inline=True
6901→ )
6902→
6903→ embed.set_footer(text="ListSync Notification System")
6904→ embed.set_timestamp()
6905→
6906→ # Add embed to webhook (only embed, no content)
6907→ webhook.add_embed(embed)
6908→
6909→ # Send webhook
6910→ response = webhook.execute()
6911→
6912→ return {
6913→ "success": True,
6914→ "message": "Test notification sent successfully! Check your Discord channel.",
6915→ "timestamp": current_time
6916→ }
6917→
6918→ except ImportError:
6919→ # Fallback to using requests directly
6920→ from datetime import datetime
6921→ current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
6922→
6923→ # Only send embed, no content to avoid duplicate messages
6924→ payload = {
6925→ "embeds": [{
6926→ "title": "🧪 Discord Integration Test",
6927→ "description": "If you see this message, Discord notifications are working correctly! ✅",
6928→ "color": 10181046,
6929→ "fields": [
6930→ {
6931→ "name": "Test Time",
6932→ "value": current_time,
6933→ "inline": True
6934→ },
6935→ {
6936→ "name": "Status",
6937→ "value": "✅ Connected",
6938→ "inline": True
6939→ }
6940→ ],
6941→ "footer": {
6942→ "text": "ListSync Notification System"
6943→ },
6944→ "timestamp": datetime.utcnow().isoformat()
6945→ }]
6946→ }
6947→
6948→ response = requests.post(webhook_url, json=payload, timeout=10)
6949→ response.raise_for_status()
6950→
6951→ return {
6952→ "success": True,
6953→ "message": "Test notification sent successfully! Check your Discord channel.",
6954→ "timestamp": current_time
6955→ }
6956→
6957→ except requests.exceptions.Timeout:
6958→ raise HTTPException(status_code=504, detail="Discord webhook request timed out")
6959→ except requests.exceptions.RequestException as e:
6960→ error_msg = f"Failed to send Discord notification: {str(e)}"
6961→ if hasattr(e, 'response') and e.response is not None:
6962→ error_msg += f" (Status: {e.response.status_code})"
6963→ raise HTTPException(status_code=500, detail=error_msg)
6964→ except Exception as e:
6965→ import traceback
6966→ error_detail = f"Failed to send test notification: {str(e)}\n{traceback.format_exc()}"
6967→ logging.error(error_detail)
6968→ raise HTTPException(status_code=500, detail=f"Failed to send test notification: {str(e)}")
三、POC概念验证
1、POC代码
管理员已设置登录后刷新可查看2、输出结果
Sandbox Execution Cancelled
++++++++++++++++++++++++++++++++++++ Dnslog ++++++++++++++++++++++++++++++++++++
Request was made from IP: 172.217.46.16, 69.28.61.220, 69.28.61.221
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3、预期行为
仅允许访问受信任的 URL
四、影响范围
Woahai321 ListSync <= 0.6.6
五、修复建议
Woahai321 ListSync > 0.6.6
六、参考链接
管理员已设置登录后刷新可查看