各类互联网产品的新用户福利场景,极易出现业务逻辑漏洞。攻击者可利用设计缺陷绕过营销限制,反复领取限时权益,若免费权益为付费资源,则直接造成运营资损。

​

这类问题几乎都源于四大底层设计疏漏:

  1. 用户身份校验体系单一,缺少多维度追溯能力;
  2. 权益发放判断逻辑简单,未核查历史操作记录;
  3. 注册、注销、营销三大业务模块完全割裂,无状态联动;
  4. 数据库存储模型设计不合理,数据丢失导致风控失效。

​

本文以一款会员时长类应用作为研究样本,完整拆解因账号注销物理删库带来的权限校验缺陷,复现 “注销 - 重注册” 无限领取新人权益的攻击链路,同时搭配抽象伪代码定位底层代码问题,给出通用化修复与审计思路。

​

文中演示代码均为抽象伪 Demo,仅用于还原设计缺陷,非业务原始源码。


漏洞实战复现(抽象业务场景描述)

测试目标为一款提供付费时长服务的会员应用,新注册用户可自动领取两天免费使用时长。(由于该APP较为敏感,这里用的平替图)

  1. 全新临时账号登录,系统自动下发两天免费时长;

  1. 等待权益剩余时长缩减至 1 天,复现真实用户使用场景;

  1. 执行账号注销、清空本地缓存,服务端物理删除当前账号全部数据;

  1. 让系统二次下发临时账号;

  1. 系统识别为全新用户,再次下发完整两天新人免费时长,循环操作可无限重置权益。


整体流程大概就是

下面进行讲解,啥情况下会导致该漏洞产生


一、用户身份校验体系存在短板

1. 仅依靠单一字段区分用户

系统仅通过邮箱、手机号作为判定新用户的唯一依据,未叠加设备指纹、IP、实名信息等辅助校验维度。一旦账号注销后数据被彻底清除,使用同一手机号 / 邮箱二次注册,系统会直接判定为全新用户。与某src的业务审核人员交流后发现,该问题在中小型应用中十分普遍:业务接口仅查询当前有效账号,完全忽略已注销的历史身份。

2. 注销数据无持久化留存

产品执行账号注销时,直接物理删除整条用户数据,没有留存注销台账、权益领取记录。本次分析的样本应用存在同类缺陷:临时测试账号数据无长期持久化机制,注销清空数据后,系统彻底丢失该身份的历史行为记录,无法做二次风控拦截。

​

配套缺陷演示:

@Service
public class VulnerableUserService {

@Autowired
private UserRepository userRepository;

// 注册逻辑:仅检查邮箱是否存在
public User register(String email, String password) {
    // 漏洞点:仅通过邮箱判断用户是否存在,未考虑已注销账户
    if (userRepository.findByEmail(email) != null) {
        throw new RuntimeException("邮箱已注册");
    }
    User newUser = new User(email, password);
    return userRepository.save(newUser);
}

// 注销逻辑:直接删除所有数据
public void deleteAccount(Long userId) {
    userRepository.deleteById(userId); 
  }
}


二、权益发放校验逻辑片面化

1. 新用户判定标准仅看当前账号状态

发放新人福利时,系统只判断当前账号是否为首次注册,没有考虑账号注销重置身份的攻击场景,攻击者循环注销重注册就能反复领取权益。

2. 不追溯身份历史领取记录

权益下发逻辑仅绑定当前有效账号,不会检索该手机号、邮箱过往所有领券行为;账号被删除后,对应的权益领取记录同步消失,风控完全失效。

​

缺陷发放逻辑演示:

@Service
public class VulnerableCouponService {

    @Autowired
    private UserRepository userRepository;

    // 发放新人权益接口
    public Coupon grantCoupon(String email) {
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new RuntimeException("用户不存在");
        }
        // 风险点:仅校验当前账号领券状态,无全生命周期历史核查
        if (user.getCoupons().isEmpty()) {
            Coupon coupon = new Coupon("NEW_USER_100");
            user.getCoupons().add(coupon);
            userRepository.save(user);
            return coupon;
        } else {
            throw new RuntimeException("当前账号已领取新人权益");
        }
    }
}


三、业务流程模块割裂,缺少风控兜底

1. 注销与营销模块无状态联动

产品设计时将账号注销、权益发放拆分为独立模块,两者数据完全不互通。注销操作不会同步更新营销活动的风控台账,无法拦截重置身份重复领福利的行为。

2. 缺少注册频率冷却风控

接口未限制同一 IP、设备、手机号的短时间注册次数,无自动化行为拦截策略,攻击者可批量脚本循环注册注销,大批量消耗运营权益。

​

无任何风控的注册接口演示:

@RestController
public class VulnerableAuthController {

    @Autowired
    private VulnerableUserService userService;

    @PostMapping("/register")
    public User register(@RequestParam String email,
                         @RequestParam String password) {
        // 风险点:无设备指纹校验、无IP注册频次限制
        return userService.register(email, password);
    }
}


四、底层数据库模型设计缺陷

  1. 未使用软删除机制,账号注销直接执行物理删除;
  2. 用户表与权益记录表采用级联删除,账号销毁后,所有领券记录同步清除,永久丢失审计凭证;
  3. 查询接口仅过滤有效账号,无法检索历史注销身份。


数据库实体缺陷演示:

@Entity
public class User {
    @Id
    @GeneratedValue
    private Long id;
    private String email;

    // 风险点:无is_deleted、deleted_at软删除标记;级联删除连带清除权益记录
    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
    private List<Coupon> coupons = new ArrayList<>();
}

public interface UserRepository extends JpaRepository<User, Long> {
    // 仅查询现存有效账号,已物理删除数据无法追溯
    User findByEmail(String email);
}


用流程图总结一下,大概就是

五、漏洞总结与标准化修复方案

漏洞根因总结

该时长会员应用出现重复领权益漏洞,核心问题是账号数据生命周期与营销风控体系脱节。物理删除注销用户数据,导致系统无法追溯身份历史行为;叠加单一字段校验、模块数据隔离、无多层风控多重缺陷,最终形成可无限复用新人福利的业务漏洞。此类逻辑漏洞无法通过扫描器发现,攻击流量和正常用户操作高度相似,是业务安全审计的重点排查对象。

分层修复落地建议

  1. 多维度身份识别叠加手机号、邮箱、设备指纹、IP 地址多维度做身份判定,不单一依靠账号表存在性判断新用户;


  1. 数据持久化追溯机制放弃纯物理删除,采用is_deleted、deleted_at软删除字段;独立搭建权益领取历史台账,账号注销不删除台账记录,永久留存领券审计数据;


  1. 打通模块状态联动注销操作同步更新营销风控台账,下发权益前优先查询身份全历史领券记录,而非仅校验当前账号;


  1. 增加接口风控兜底对注册接口增加频次限制、短时间注册冷却机制,拦截自动化批量注销注册行为。