跳转到正文
报告库
用途分类 / 其他用途

Extension Email Marketing Skill 安全审计

作者说它能做什么(原文)

Send personalised marketing emails to subscribers with an unsubscribe link.

第三方安全检查结论

先别安装或运行

已检查文件
1
发现的风险
3
会不会运行危险命令?检查是否下载程序后直接运行、让他人远程控制电脑,或藏起要运行的命令。未发现风险
会不会泄露文件和密钥?检查是否发送含密码或密钥的文件,以及代码里是否直接写了密钥。发现 2 项风险
高风险

任何调用者都能先占用他人的邮箱并将其关联到自己的身份

原文依据:3 处
发现了什么

注册接口接受调用者提供的任意邮箱,在证明邮箱所有权之前就创建调用者与邮箱的关联、占用唯一性记录并加入营销主题。之后才发送验证邮件。

为什么需要注意

攻击者可用受害者邮箱注册,使真正持有人因“Email already taken”而无法注册。若持有人点击收到的验证链接,该邮箱还可能被验证为攻击者主体所建资料的邮箱,并启用该资料的营销投递资格。

注册接口在验证邮箱归属前,就把调用者提供的任意邮箱写入其个人资料和全局唯一集合。其他人随后使用该邮箱会收到“Email already taken”。因此,只要此示例被实际采用,调用者就可能抢占他人的邮箱并阻止真实所有者注册;后续发送验证邮件并不会撤销已建立的关联。用户可要求作者在验证成功前不要永久占用邮箱,并限制未验证记录的权限和有效期。

SKILL.md:186来自说明文档打开原文件
  public shared ({ caller }) func registerUser(name : Text, email : Text) : async () {    // Check if the user already exists    if (userProfiles.containsKey(caller)) {      Runtime.trap("User already registered");    };    // Check if the email is already used    if (emails.contains(email)) {      Runtime.trap("Email already taken");    };    // Add a user record    userProfiles.add(      caller,      {        name;        email;      },    );    emails.add(email);    // Subscribe the user to the Newsletter topic by default
查看另外 2 个位置
SKILL.md:204来自说明文档打开原文件
    emails.add(email);    // Subscribe the user to the Newsletter topic by default    let topicId = EmailSubscribers.getTopicId(emailSubscribers, newsletterTopic)      ?? Runtime.trap("Newsletter topic not found");    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email    let result = await EmailClient.sendVerificationEmail(      "no-reply",      [email],      "Welcome to Our Service",      "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team",    );
SKILL.md:208来自说明文档打开原文件
    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email    let result = await EmailClient.sendVerificationEmail(      "no-reply",      [email],      "Welcome to Our Service",      "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team",    );
中风险

验证邮件发送失败后可能留下无法重试的部分注册状态

原文依据:2 处
发现了什么

示例先保存用户、占用邮箱并添加订阅,然后跨异步调用发送验证邮件;失败分支只触发错误,没有清除此前状态。

为什么需要注意

如果异步调用前的状态已经提交,临时邮件服务故障会留下未验证但已占用的邮箱和用户记录。相同调用者或邮箱重试时会被“already registered/taken”拒绝,用户可能无法自行恢复。

示例在等待邮件服务结果之前,已经写入个人资料、唯一邮箱集合和订阅状态。邮件返回错误时仅触发异常,源码没有显示删除这些写入、重发入口或过期机制。因此,在这些写入不会由运行时自动回滚的部署条件下,失败可能留下未验证但已占用的邮箱,使同一调用者和邮箱都无法按此接口重新注册。用户可要求作者说明异步失败的事务语义,并提供幂等重试或清理机制。

SKILL.md:195来自说明文档打开原文件
    };    // Add a user record    userProfiles.add(      caller,      {        name;        email;      },    );    emails.add(email);    // Subscribe the user to the Newsletter topic by default    let topicId = EmailSubscribers.getTopicId(emailSubscribers, newsletterTopic)      ?? Runtime.trap("Newsletter topic not found");    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email
查看另外 1 个位置
SKILL.md:208来自说明文档打开原文件
    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email    let result = await EmailClient.sendVerificationEmail(      "no-reply",      [email],      "Welcome to Our Service",      "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team",    );    switch (result) {      case (#ok) {};      case (#err(error)) {        Runtime.trap("Failed to send verification email: " # error);      };    };  };
会不会删除文件或一直在后台运行?检查是否大范围删除文件、改写磁盘,或设置自动启动。未发现风险
会不会绕过安全保护?检查是否跳过网站安全验证、开放过多文件权限,或取消操作前的确认。未发现风险
会不会误导 AI 或隐藏内容?检查工作说明是否要求 AI 忽略你的指令、干扰检查结果,或夹带看不见的文字。未发现风险
会不会偷偷改推广链接或收款方?检查是否强制替换推广链接或收款对象,同时要求隐瞒更改。发现 1 项风险
中风险

注册会默认加入营销主题,并把邮箱验证与营销同意捆绑在一次点击中

原文依据:3 处
发现了什么

示例在注册时直接添加 Newsletter 订阅,验证邮件随后声称点击验证链接也代表同意月度营销邮件。用户没有独立、明确的营销选择。

为什么需要注意

只想验证账户邮箱的用户可能在同一操作中被记为营销同意,导致不符合其真实选择的广告邮件,并可能给使用方带来同意证明和合规争议。

示例在注册过程中直接把邮箱加入 Newsletter,没有单独的营销选择;验证邮件又将“验证邮箱”和“同意订阅月报”合并为同一次点击。虽然邮件告知可以退订,且技能规定只有已验证的订阅者才能收信,但这不能替代独立、明确的营销同意。若应用照搬示例,用户可能为完成邮箱验证而同时被登记为营销订阅者。用户可要求作者提供默认不勾选的独立订阅步骤,并记录同意时间与主题。

SKILL.md:204来自说明文档打开原文件
    emails.add(email);    // Subscribe the user to the Newsletter topic by default    let topicId = EmailSubscribers.getTopicId(emailSubscribers, newsletterTopic)      ?? Runtime.trap("Newsletter topic not found");    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email    let result = await EmailClient.sendVerificationEmail(      "no-reply",      [email],      "Welcome to Our Service",      "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team",    );
查看另外 2 个位置
SKILL.md:208来自说明文档打开原文件
    ignore EmailSubscribers.add(emailSubscribers, topicId, email);    // Send a verification email    let result = await EmailClient.sendVerificationEmail(      "no-reply",      [email],      "Welcome to Our Service",      "Hello " # name # ",<br><br>Thank you for registering with our service.<br><br>Please <a href=\"{{VERIFICATION_URL}}\">click here</a> to verify your email address.<br><br>By clicking on the verification link you also agree to sign-up to the monthly Newsletter which you can unsubscribe from at any time.<br><br>Best regards,<br>The Team",    );    switch (result) {
SKILL.md:24来自说明文档打开原文件
- Users MUST have verified their email address AND MUST be subscribed to a marketing topic before they can receive marketing emails on that topic- Marketing emails MUST contain an unsubscribe link which will unsubscribe the user from the given topic- This component depends on the [extension-email-verification](../extension-email-verification/SKILL.md) for verifying email addresses, be sure to check that too.

Skill 逻辑拆解

1 个说明模块

该 Skill 的示例把订阅者状态、邮箱验证状态和用户资料分别保存,并通过预制模块管理主题订阅。营销邮件仅从已验证的主题订阅者中选择收件人。

查看原文
SKILL.md:261来自说明文档打开原文件
    };    // Get the array of subscriber emails that have been verified    let recipientEmails = EmailSubscribers.verified(emailSubscribers, verifiedEmails, topicId)      ?? Runtime.trap("No verified subscribers found for newsletter topic");    if (recipientEmails.size() == 0) {      Runtime.trap("No verified subscribers found for newsletter topic");    };
SKILL.md:282来自说明文档打开原文件
    );    let result = await EmailClient.sendMarketingEmail(      topicId,      "no-reply",      recipients,      subject,      finalHtmlBody,    );    switch (result) {

发送营销邮件和列出订阅者需要管理员权限;订阅与退订接口则作用于调用者本人资料中的邮箱。

查看原文
SKILL.md:247来自说明文档打开原文件
  public shared ({ caller }) func subscribeToTopic(topicId : Nat) : async () {    let userProfile = getUserInternal(caller);    ignore EmailSubscribers.add(emailSubscribers, topicId, userProfile.email);  };  public shared ({ caller }) func unsubscribeFromTopic(topicId : Nat) : async () {    let userProfile = getUserInternal(caller);    EmailSubscribers.remove(emailSubscribers, topicId, userProfile.email);  };
SKILL.md:257来自说明文档打开原文件
  public shared ({ caller }) func sendMarketingEmail(topicId : Nat, subject : Text, htmlBody : Text) : async () {    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {      Runtime.trap("Unauthorized: Only admins can send the newsletter");    };    // Get the array of subscriber emails that have been verified
SKILL.md:301来自说明文档打开原文件
  // Admin function to list topic subscribers and whether the email is verified or not  public query ({ caller }) func listSubscribers(topicId : Nat) : async [(Text, Bool)] {    if (not (AccessControl.hasPermission(accessControlState, caller, #admin))) {      Runtime.trap("Unauthorized: Only admins can list topic subscribers");    };    EmailSubscribers.list(emailSubscribers, verifiedEmails, topicId).get([]);  };

示例会在缺少退订占位符时自动追加退订链接,并包含处理退订链接的预制模块;该模块的实现未包含在所提供来源中,因此无法独立核实其令牌校验和访问控制。

查看原文
SKILL.md:90来自说明文档打开原文件
Use the prefabricated module `caffeineai-email-marketing/unsubscribeMixin.mo` which cannot be modified.The MixinEmailUnsubscribe module handles calls to the unsubscribe link to unsubscribe an email address from a topic.
SKILL.md:267来自说明文档打开原文件
    };    // Ensure the email body contains the unsubscribe link placeholder    let finalHtmlBody = if (htmlBody.contains(#text "{{UNSUBSCRIBE_URL}}")) {      htmlBody;    } else {      htmlBody # "<br><br>To unsubscribe <a href=\"{{UNSUBSCRIBE_URL}}\">click here</a>";    };    // For each recipient specify the NAME substitution to personalise the email
从这里开始 · 工作说明SKILL.md
extension-email-marketing
连线表示工作说明包含的模块,不是实际运行顺序。点击模块可查看原文。
文件与检查记录1 个文件

检查范围与遗漏

逐文件查看涉及的内容

下方列出本次涉及的原文范围;纳入检查不代表已查清所有问题。

  • SKILL.md已纳入全文

这份报告只针对上方版本。我们看了拿到的代码和说明文件,没有实际运行 Skill,也没有检查它另外安装的软件包。因此,这不是“保证安全”的承诺;换了版本或使用环境,结果也可能不同。

  • SKILL.md工作说明

代码和说明中提到的操作

连接外部网站
SKILL.md:14来自说明文档打开原文件
# Email — MarketingMarketing email extension for [Caffeine AI](https://caffeine.ai?utm_source=caffeine-skill&utm_medium=referral).
读取了多少行
357
文件校验值(用于核对版本)
f88f49f3f0556b43df99da027cd61482db4316f08f613834178e1ff85beeeeae