零、项目代码和攻击面介绍

                众嗦粥汁,一个spring boot项目里DemoAplication是启动器,各个Controller是主要的攻击面,config是副攻击面,现在我通过AI写了一个spring boot项目并分析它的攻击点,以下是Usercontroller的代码和整个项目的结构

package com.liangning.demo.controller;

import com.liangning.demo.common.Result;
import com.liangning.demo.entity.User;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

    private final JdbcTemplate jdbcTemplate;

    public UserController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // ✅ 查询用户:/user/get?id=1
    @GetMapping("/get")
    public Result<User> getUser(@RequestParam Long id) {
        String sql = "SELECT id, name FROM users WHERE id = ?";
        Map<String, Object> row = jdbcTemplate.queryForMap(sql, id);

        User u = new User();
        u.setId(((Number) row.get("id")).longValue());
        u.setName((String) row.get("name"));

        return Result.ok(u);
    }

    // ✅ 新增用户:/user/add?name=tom
    @GetMapping("/add")
    public Result<Long> addUser(@RequestParam String name) {
        String sql = "INSERT INTO users(name) VALUES (?)";
        jdbcTemplate.update(sql, name);

        // 取最后插入的自增 id(对 MySQL 有效)
        Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);

        return Result.ok(id);
    }

    @GetMapping("/getUnsafe")
    public Object getUserUnsafe(@RequestParam String id) {
        String sql = "SELECT id, name FROM users WHERE id = " + id;
        System.out.println("SQL=" + sql);
        return jdbcTemplate.queryForMap(sql);
    }

    @GetMapping("/getSafe")
    public Object getUserSafe(@RequestParam int id) {
        String sql = "SELECT id, name FROM users WHERE id = ?";
        System.out.println("SQL(template)=" + sql + ", param=" + id);
        return jdbcTemplate.queryForMap(sql, id);
    }


}

一、引言(短、聚焦)

SQL 注入是什么?

简要解释攻击原理:

SQL 注入是攻击者通过构造恶意输入,使得用户输入被错误地当成 SQL 语句的一部分执行,从而泄露或非法操作数据库。
例如:id=1 OR 1=1 会改变原本预期的条件语义。

说明它仍然是 Web 安全的基础性问题。


二、为什么 Spring Boot 也可能存在 SQL 注入风险

即使框架自动配置方便,仍可能存在漏洞,原因包括:

  • 直接字符串拼接 SQL(危险)

  • JDBC Statement 或拼接拼出来的查询

  • 错误处理机制泄露信息(堆栈/SQL)

  • 动态 SQL / 手写查询未做好参数化

可以引用“Spring Boot 默认方便但不无敌”的观点。


三、真实案例:可复现的 SQL 注入示例

3.1 不安全版代码(实战)

    @GetMapping("/getUnsafe")
    public Object getUserUnsafe(@RequestParam String id) {
        String sql = "SELECT id, name FROM users WHERE id = " + id;
        System.out.println("SQL=" + sql);
        return jdbcTemplate.queryForMap(sql);
    }
    

    攻击输入示例:

    GET /user/getUnsafe?id=1 OR 1=1
    

    结果

    这说明攻击成立 & 信息泄露严重。

    1) status=500

    服务器内部错误
    不是“找不到页面”(404),也不是“你没权限”(403),而是后端代码执行过程中抛异常了

    2) Incorrect result size: expected 1, actual 3

    这句最关键:
    期望只查到 1 行,但实际查到了 3 行

    它在告诉你:你后端写的查询方式是“我只要一条记录”,但数据库返回了多条。

    四、安全修复策略(多层面)


    4.1 参数化查询(核心)

    正确写法:

    @GetMapping("/getSafe")
    public Object getUserSafe(@RequestParam int id) {
        String sql = "SELECT id, name FROM users WHERE id = ?";
        return jdbcTemplate.queryForMap(sql, id);
    }
    

    对比行为:

    • 输入 1 OR 1=1400 Bad Request / 拒绝执行

    • SQL 不被注入,参数边界立起来

    解释参数化机制为什么防注入(参数与 SQL 分离)。

    结果为

    但是仍然有错误信息泄露的问题

    4.2 全局统一异常处理(错误不泄露)

    解决方案:

    内容为

    
    package com.liangning.demo.exception;
    import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import java.util.HashMap;
    import java.util.Map;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(MethodArgumentTypeMismatchException.class)
        public ResponseEntity<Map<String, Object>> handleBadRequest(MethodArgumentTypeMismatchException e) {
            System.err.println("[400] Bad request: " + e.getMessage());
    
            Map<String, Object> body = new HashMap<>();
            body.put("code", 400);
            body.put("msg", "Bad Request");
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<Map<String, Object>> handleServerError(Exception e) {
            e.printStackTrace();
    
            Map<String, Object> body = new HashMap<>();
            body.put("code", 500);
            body.put("msg", "Internal Server Error");
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(body);
        }
    }
    

    目的:

    • 前端不再看到具体堆栈、类名、SQL

    • 后端日志保留详细信息

    • 所有异常返回统一结构

    这是工程级防护,不只防注入,还把错误信息泄露降为最小。

    4.3 输入验证/转换边界(加强安全)

    参数化之外,还应该:

    • 对输入类型/格式做验证(如 @Valid, Bean Validation)

    • 对字符串或复杂条件输入设定提前拒绝

    这是 “提前在边界碰掉危险输入” 的策略。


    五、进一步的防护建议(进阶)

    原文可能只提到了参数化,这里增加几点实战建议

    5.1 使用 ORM/框架自动处理

    例如 Spring Data JPA 或 MyBatis :

    • MyBatis: 使用 #{param} 避免注入
      不要用 ${param} 直接拼接。


    5.2 做最小权限原则(数据库)

    即使注入成功,也限制账户权限范围,降低破坏面。


    5.3 输入过滤/编码层

    对用户输入进行正则或规范范围验证(长度/字符/类型验证)。
    可与 Bean Validation 或 HTTP 过滤器结合。

    六、结果对照 + 验收方法(工程级)

    场景 预期 实际
    不安全 SQL 直接注入成功 SQL 改写
    安全 SQL(参数化) 注入失败 类型转换/查询无结果
    异常展示 堆栈 + SQL JSON 错误,不泄露细节

    • 未修复前白页堆栈

    • 修复后统一 JSON / 400/500

    • SQL 模板 vs 攻击改写显式对比


    七、总结(工程视角)

    • SQL 注入仍是常见漏洞

    • 参数化查询与 ORM 是最核心防御

    • 错误信息泄露也是重要攻击面

    • 输入验证/最小权限是强化手段

    • 真实工程需要全流程闭环验证

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐