CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
代码审查入门/课时/安全审查
RawBlame
课时目录12 节

代码审查入门

  1. 01API 语义审查
  2. 02实现语义
  3. 03文档审查
  4. 04测试审查
  5. 05代码风格
  6. 06Review 流程
  7. 07审查清单
  8. 08安全审查
  9. 09AI 辅助审查
  10. 10反馈技巧
  11. 11团队实践
  12. 12综合练习

安全审查

密钥与注入风险。

30 分钟

安全审查

这一课你将能做到

  1. 在 Code Review 中识别 SQL 注入、越权访问、敏感信息泄露三类高危漏洞
  2. 判断加密、鉴权、审计日志的实现是否满足最小权限和可溯源要求
  3. 建立「安全红线」清单,把 P0 安全问题变成合并阻断项

为什么要学

安全漏洞的成本不是修复一行代码,而是「通知所有用户改密码 + 监管罚款 + 品牌信任崩塌」。很多安全 bug 极其容易审查——SQL 注入、硬编码密钥、接口没鉴权——但 review 时没人专门看安全视角。

人话:安全审查不需要你是安全专家,只需要你在 review 时多问几句「这里能不能被恶意利用」。

先建立图像

CR 安全审查的「红蓝对抗」视角

蓝方(防御)                    红方(攻击者想做什么)
├─ SQL 全部参数化               → 注入数据库
├─ 接口入口鉴权(注解/拦截器)    → 越权访问别人的数据
├─ 输出编码(防 XSS)           → 把 JS 脚本塞给用户
├─ 密钥不落地代码               → 翻到密钥进生产环境
├─ 文件上传限制类型/大小          → 上传 webshell
├─ 日志脱敏                    → 日志泄露用户隐私
└─ 操作审计日志                  → 事后溯源

审查口诀:输入不轻信、输出必编码、鉴权在入口、密钥不落地。

跟着做

步骤 1:审 SQL 注入

❌ 反例:字符串拼接 SQL

@Select("SELECT * FROM course WHERE title LIKE '%" + keyword + "%'")
List<Course> search(String keyword);

攻击者输入 keyword = "'; DROP TABLE course; --",全站课程表消失。

✅ 正例:MyBatis #{} 预编译参数化

@Select("SELECT * FROM course WHERE title LIKE CONCAT('%', #{keyword}, '%')")
List<Course> search(@Param("keyword") String keyword);
// MyBatis XML 写法
<select id="search" resultType="Course">
    SELECT * FROM course
    WHERE title LIKE CONCAT('%', #{keyword}, '%')
</select>

审查动作:搜索 PR 里的所有 SQL,看有没有 ${}(字符串替换)或字符串拼接。 #{} 是参数绑定,安全;${} 是字符串替换,危险。

步骤 2:审接口鉴权

❌ 反例:接口没鉴权

@GetMapping("/api/v1/courses/{id}/students")
public List<UserVO> getStudents(@PathVariable Long id) {
    // 没检查当前用户是否有权限查看这门课的学生
    return courseService.getStudents(id);
}

攻击者遍历 id,拿到所有课程的所有学生名单。

✅ 正例:Spring Security + 业务层鉴权

@PreAuthorize("@courseAuthChecker.isOwner(#id, authentication.name)")
@GetMapping("/api/v1/courses/{id}/students")
public List<UserVO> getStudents(@PathVariable Long id) {
    return courseService.getStudents(id);
}

或者统一在 Service 层兜底:

public List<UserVO> getStudents(Long courseId) {
    Long currentUserId = SecurityUtils.getCurrentUserId();
    Course course = courseMapper.selectById(courseId);
    if (course == null || !course.getAuthorId().equals(currentUserId)) {
        throw new AccessDeniedException("无权查看此课程的学生");
    }
    return studentMapper.listByCourse(courseId);
}

审查动作:看 Controller 每个接口有没有 @PreAuthorize、@Secured、或 Service 层权限检查。 特别是涉及「查看/修改别人的数据」的接口。

步骤 3:审敏感信息

❌ 反例:

@Value("${aliyun.accessKeySecret}")
private String accessKeySecret;   // 密钥写在代码里或 properties 里提交到仓库

// 日志打印用户信息
log.info("用户登录 user={}", user);   // 打印了 password 字段

// 错误响应把堆栈暴露出去
return Result.fail(e.getMessage() + " " + e.getStackTrace());

✅ 正例:

// 密钥走环境变量 / Vault / 阿里云 KMS
@Value("${ALIYUN_ACCESS_KEY_SECRET}")   // 从环境变量注入,不提交到代码仓库
private String accessKeySecret;

// 日志脱敏
log.info("用户登录 userId={}", user.getId());   // 只打非敏感字段

// 错误响应只返回业务码
return ResponseEntity.status(500)
        .body(new ErrorBody("INTERNAL_ERROR", "系统错误,请联系管理员", traceId));

审查动作:

  • 搜索 PR 里的 password、secret、token、key 关键词
  • 检查日志打印对象时是否脱敏
  • 检查异常处理是否暴露内部信息

步骤 4:审 XSS 与输出编码

❌ 反例:直接把用户输入渲染到页面

// Next.js 前端
function Comment({ content }) {
    return <div>{content}</div>;   // content 里塞 <script>alert(1)</script> → XSS
}

✅ 正例:React 默认转义 + 富文本用 DOMPurify

import DOMPurify from 'dompurify';

function Comment({ content }) {
    // 纯文本:React 默认转义,安全
    return <div>{content}</div>;
}

function RichComment({ htmlContent }) {
    // 富文本:用 DOMPurify 过滤危险标签
    const clean = DOMPurify.sanitize(htmlContent, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
    });
    return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

审查动作:看到 dangerouslySetInnerHTML 时必须有 Purify;后端返回 HTML 时必须有转义或白名单。

步骤 5:审文件上传

❌ 反例:

@PostMapping("/upload")
public String upload(MultipartFile file) throws IOException {
    File dest = new File("/uploads/" + file.getOriginalFilename());
    file.transferTo(dest);
    return "/uploads/" + file.getOriginalFilename();
}

攻击者上传 ../../etc/passwd 或 shell.jsp。

✅ 正例:限制扩展名、限制大小、重命名存储、非 Web 目录

@PostMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file) {
    String ext = FilenameUtils.getExtension(file.getOriginalFilename());
    List<String> allowed = List.of("jpg", "png", "pdf");
    if (!allowed.contains(ext.toLowerCase())) {
        throw new BizException("不支持的文件类型");
    }
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new BizException("文件超过 10MB 限制");
    }

    String storedName = UUID.randomUUID() + "." + ext;
    Path dest = Paths.get(uploadPath, storedName);   // uploadPath 在配置里,不是 Web 目录
    file.transferTo(dest);
    return storedName;
}

讲清楚要点

  1. 安全审查的核心是「不信任任何输入」:不管是 URL 参数、请求体、文件、Header,都可能被攻击者控制。审查时对每个外部输入问「如果这里塞恶意内容会怎样」。
  2. 鉴权要在每个入口做:前端校验是防君子,后端校验才是防小人。所有涉及数据隔离的接口,后端必须有权限校验。
  3. 密钥不落地代码:代码仓库里不该有密码、密钥、Token。走环境变量、Vault、KMS。
  4. 日志是安全漏洞的重灾区:打 user 对象可能把密码带出来;打 SQL 可能把身份证号带出来。敏感字段必须脱敏。
  5. 安全红线就是 P0:安全漏洞没有任何「下次再修」的空间——一旦合入,风险就存在。

你可能会懵的地方

  • MyBatis ${} 什么时候能用? 只有「不是用户输入」的场景:比如 ORDER BY ${column} 里的 column 是后端枚举白名单控制的。审查时看到 ${} 先标红,再让作者证明「这个变量不可被用户控制」。

  • 接口鉴权用 Spring Security 注解还是代码里写 if? 粗粒度(角色级别)用注解(@PreAuthorize("hasRole('ADMIN')"));细粒度(数据归属级别)用代码判断(当前用户是不是这条数据的 owner)。两者结合使用。

  • JWT Token 泄露了怎么办? Token 不该出现在日志里(脱敏);Token 有效期要短(access token 15 分钟,refresh token 7 天);支持撤销(Redis 存黑名单或 token 版本号)。审查时看到 token 处理问这三个问题。

  • XSS 后端要不要防? 最佳实践是「输出端编码」:后端存原始数据,前端渲染时转义。如果后端也做过滤,可能导致数据被错误修改(比如用户故意写了 <> 当文本用)。审查时看渲染端有没有做编码。

  • 敏感信息脱敏到什么程度? 日志里:手机号 138****8888、身份证 110**********1234、密码绝不出现;API 响应里:密码不回传(即使是当前用户自己的),密钥不回传;错误信息里:不暴露堆栈、不暴露数据库表结构。

错法对照

错法问题正解
SQL 字符串拼接SQL 注入#{} 参数化
接口无鉴权越权访问@PreAuthorize / 业务层校验
密钥写死在代码/配置密钥泄露环境变量 / Vault
日志打印完整用户对象隐私泄露脱敏 / 只打非敏感字段
文件上传不限制类型/路径上传 webshell白名单扩展名 + 重命名 + 非 Web 目录

卡点急救

  1. 怀疑 SQL 注入但不确定:用 SQLMap 或手动构造 ' OR '1'='1 payloads 测试。如果接口返回了不该返回的数据,就是注入。

  2. 接口太多,不知道哪个没鉴权:用 Burp Suite 或 Postman 批量遍历,用两个不同用户的 Token 调同一个接口看数据是否隔离。review 时优先审查「增删改 + 查别人的数据」类接口。

  3. 密钥已经泄露到仓库了怎么办:立刻 revoke(阿里云/AWS 控制台撤销密钥),然后历史提交也清理掉(BFG Repo-Cleaner 或 filter-branch)。

面试常问

  • Code Review 时怎么发现 SQL 注入? 搜所有 SQL:看有没有字符串拼接(+ 拼接 SQL)、${} 占位符、动态 SQL 里未白名单的 ORDER BY / GROUP BY 字段。MyBatis 里 #{} 是安全的(预编译),${} 是危险的(字符串替换)。

  • 越权和未授权的区别?怎么防? 未授权 = 没登录就能调;越权 = 登录了但访问了别人的数据。防未授权用认证拦截器;防越权用数据归属校验(assert ownerId == currentUserId)。最容易漏的是「列表接口」——查询条件没限制 owner,导致能查别人的列表。

  • JWT 和 Session 各有什么安全问题? JWT:不可撤销(除非额外做黑名单)、payload 可被客户端读取、体积大。Session:服务端集中存储,分布式下需要共享(Redis),但可撤销、可刷新。审查时 JWT 要问「怎么撤销被盗的 Token」;Session 要问「Session 固定攻击怎么防」。

自测三问

  1. MyBatis 里 #{} 和 ${} 在安全上的根本区别是什么?
  2. 为什么说「前端校验是防君子,后端校验才是防小人」?
  3. 日志里打印 user.toString() 可能泄露什么敏感信息?

小结一句话

安全审查的口诀是「输入不轻信、输出必编码、鉴权在入口、密钥不落地」——每处外部输入都假设它带着恶意。

延伸

  • OWASP Top 10
  • OWASP Code Review Guide
  • Spring Security 官方文档
  • 下一课:AI 辅助审查——Bugbot 与静态分析对照。
加载本节练习…
← 上一课审查清单下一课 →AI 辅助审查