JavaScript安全:前端常见的XSS和CSRF攻击原理与防御措施
大家好,今天我们来聊聊JavaScript前端安全中两个非常重要的威胁:跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。我会深入讲解它们的原理,并提供实用的防御措施。
一、跨站脚本攻击 (XSS)
XSS 攻击允许攻击者将恶意 JavaScript 代码注入到其他用户的浏览器中。这些恶意代码可以窃取用户的 Cookie、会话信息,甚至可以模拟用户执行操作。
1.1 XSS 攻击的类型
主要有三种类型的 XSS 攻击:
-
存储型 XSS (Stored XSS): 恶意脚本被永久存储在目标服务器上,例如数据库、留言板、博客评论等。当用户访问包含恶意脚本的页面时,脚本就会执行。
-
反射型 XSS (Reflected XSS): 恶意脚本通过 URL 参数、POST 数据等方式传递给服务器,服务器未经处理直接返回给用户。当用户点击包含恶意脚本的链接或提交包含恶意脚本的表单时,脚本就会执行。
-
DOM 型 XSS (DOM-based XSS): 恶意脚本不经过服务器,完全在客户端执行。攻击者通过修改页面的 DOM 结构,使得恶意脚本被执行。
1.2 存储型 XSS 攻击示例
假设有一个简单的留言板应用,用户可以在上面发布留言。如果应用没有对用户输入进行合适的过滤,攻击者可以发布包含恶意 JavaScript 代码的留言:
<form id="messageForm">
<textarea id="messageContent" name="message" placeholder="请输入留言"></textarea>
<button type="submit">发布</button>
</form>
<div id="messageBoard">
<!-- 留言列表 -->
</div>
<script>
const messageForm = document.getElementById('messageForm');
const messageBoard = document.getElementById('messageBoard');
messageForm.addEventListener('submit', (event) => {
event.preventDefault();
const message = document.getElementById('messageContent').value;
// 将留言添加到留言板
const messageElement = document.createElement('div');
messageElement.textContent = message;
messageBoard.appendChild(messageElement);
// 模拟后端存储 (实际场景中,留言会存储在数据库中)
localStorage.setItem('messages', localStorage.getItem('messages') ? localStorage.getItem('messages') + message : message);
// 清空输入框
document.getElementById('messageContent').value = '';
loadMessages();
});
// 加载留言
function loadMessages() {
messageBoard.innerHTML = '';
const messages = localStorage.getItem('messages');
if (messages) {
const messageArray = messages.split(' '); // 简单分隔,用于模拟多个留言
messageArray.forEach(message => {
const messageElement = document.createElement('div');
messageElement.innerHTML = message; // 注意:这里存在 XSS 漏洞!
messageBoard.appendChild(messageElement);
});
}
}
loadMessages(); // 页面加载时加载留言
</script>
攻击者可以输入以下内容:
<script>alert('XSS Attack!')</script>
当其他用户访问这个留言板时,这段恶意脚本就会被执行,弹出一个警告框。更危险的是,攻击者可以使用这段脚本窃取用户的 Cookie 并发送到攻击者的服务器。
1.3 反射型 XSS 攻击示例
假设有一个搜索功能,用户可以通过 URL 参数传递搜索关键词。如果应用没有对 URL 参数进行合适的过滤,攻击者可以构造包含恶意 JavaScript 代码的 URL:
<form id="searchForm">
<input type="text" id="searchInput" name="query" placeholder="搜索...">
<button type="submit">搜索</button>
</form>
<div id="searchResults">
<!-- 搜索结果 -->
</div>
<script>
const searchForm = document.getElementById('searchForm');
const searchResults = document.getElementById('searchResults');
searchForm.addEventListener('submit', (event) => {
event.preventDefault();
const query = document.getElementById('searchInput').value;
const url = `/search?query=${query}`; // 模拟 URL
window.location.href = url;
});
// 从 URL 获取搜索关键词
function getQueryParameter(name) {
const url = window.location.href;
name = name.replace(/[[]]/g, '\$&');
const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/+/g, ' '));
}
const searchQuery = getQueryParameter('query');
if (searchQuery) {
searchResults.innerHTML = `您搜索的是:${searchQuery}`; // 注意:这里存在 XSS 漏洞!
}
</script>
攻击者可以构造以下 URL:
/search?query=<script>alert('XSS Attack!')</script>
当用户点击这个 URL 时,这段恶意脚本就会被执行,弹出一个警告框。
1.4 DOM 型 XSS 攻击示例
假设有一个页面,它使用 window.location.hash
来获取参数,并将其显示在页面上。
<div id="output"></div>
<script>
const output = document.getElementById('output');
output.innerHTML = decodeURIComponent(window.location.hash.substring(1)); // 注意:这里存在 XSS 漏洞!
</script>
攻击者可以构造以下 URL:
/page.html#<img src=x onerror=alert('XSS Attack!')>
当用户访问这个 URL 时,这段恶意脚本就会被执行,弹出一个警告框。因为 onerror
事件会在图片加载失败时触发。
1.5 XSS 防御措施
-
输入验证和过滤 (Input Validation and Filtering): 对所有用户输入进行验证和过滤,包括来自表单、URL 参数、Cookie 等的数据。 验证数据类型、长度、格式等。 过滤掉或转义特殊字符,例如
<
、>
、"
、'
、&
等。-
白名单: 相比于黑名单,白名单更安全。只允许特定的字符或 HTML 标签。
-
转义: 将特殊字符转换为 HTML 实体。例如,将
<
转换为<
,将>
转换为>
。
function escapeHtml(string) { return string.replace(/[&<>"']/g, function(m) { switch (m) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case "'": return '''; default: return m; } }); } // 示例 const userInput = '<script>alert("XSS");</script>'; const escapedInput = escapeHtml(userInput); console.log(escapedInput); // 输出:<script>alert("XSS");</script>
-
-
输出编码 (Output Encoding): 在将数据输出到 HTML 页面时,进行合适的编码。
-
HTML 编码: 用于将数据插入到 HTML 标签之间。使用
escapeHtml
函数进行编码。 -
URL 编码: 用于将数据插入到 URL 中。使用
encodeURIComponent
函数进行编码。 -
JavaScript 编码: 用于将数据插入到 JavaScript 代码中。需要更加小心,避免破坏 JavaScript 代码的语法。
-
-
使用 Content Security Policy (CSP): CSP 是一种安全策略,可以限制浏览器可以加载的资源,从而减少 XSS 攻击的风险。
-
可以通过 HTTP 响应头或 HTML
<meta>
标签来设置 CSP。 -
常用的 CSP 指令:
-
default-src
: 设置所有资源的默认来源。 -
script-src
: 设置 JavaScript 脚本的来源。 -
style-src
: 设置 CSS 样式的来源。 -
img-src
: 设置图片的来源。 -
connect-src
: 设置 XMLHttpRequest、WebSocket 和 EventSource 连接的来源。 -
font-src
: 设置字体的来源。 -
object-src
: 设置<object>
、<embed>
和<applet>
标签的来源。 -
media-src
: 设置<audio>
、<video>
和<track>
标签的来源。 -
frame-src
: 设置<iframe>
和<frame>
标签的来源。 -
base-uri
: 设置<base>
标签的 URL。 -
form-action
: 设置<form>
标签的 action URL。
-
<!-- 通过 meta 标签设置 CSP --> <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">
这个 CSP 策略表示:
default-src 'self'
: 默认情况下,只允许从同源加载资源。script-src 'self' 'unsafe-inline'
: 允许从同源加载 JavaScript 脚本,并允许执行内联脚本。 ('unsafe-inline'
需要谨慎使用,因为它会降低 CSP 的安全性。)style-src 'self' 'unsafe-inline'
: 允许从同源加载 CSS 样式,并允许执行内联样式。img-src 'self' data:
: 允许从同源加载图片,并允许使用 data URI。
-
-
使用安全的 JavaScript 框架和库: 一些 JavaScript 框架和库,例如 React、Angular 和 Vue.js,提供了内置的 XSS 防御机制。它们会自动转义用户输入,并使用安全的 API 来操作 DOM。
-
设置 HttpOnly Cookie: 通过设置 Cookie 的
HttpOnly
属性,可以防止 JavaScript 代码访问 Cookie。这可以防止攻击者通过 XSS 攻击窃取用户的 Cookie。// 在服务器端设置 HttpOnly Cookie document.cookie = "sessionId=123456789; HttpOnly";
-
使用 Subresource Integrity (SRI): SRI 是一种安全机制,可以验证从 CDN 加载的资源的完整性。通过在
<script>
和<link>
标签中添加integrity
属性,可以确保浏览器加载的资源没有被篡改。<script src="https://example.com/script.js" integrity="sha384-oqVuAfW98GFTpiCZ6ipmnNWg3MGzcifm9IbX6Yqm6veY+nqUbfTIWuHqpcvvUvEA" crossorigin="anonymous"></script>
integrity
属性的值是一个哈希值,用于验证资源的完整性。crossorigin="anonymous"
属性用于允许跨域加载资源。
1.6 DOM 型 XSS 的特殊防御
DOM 型 XSS 由于完全在客户端发生,防御方式略有不同,更需要关注客户端代码的安全性。
-
避免使用
eval()
函数:eval()
函数可以将字符串作为 JavaScript 代码执行,这很容易导致 XSS 攻击。 -
避免直接操作 DOM: 尽量使用安全的 API 来操作 DOM,例如
textContent
代替innerHTML
。 -
小心处理 URL 参数: 对从
window.location.hash
、window.location.search
等获取的 URL 参数进行严格的验证和过滤。
二、跨站请求伪造 (CSRF)
CSRF 攻击允许攻击者冒充用户发起恶意请求。攻击者通过构造恶意链接或表单,诱导用户点击或提交,从而在用户不知情的情况下执行操作。
2.1 CSRF 攻击的原理
CSRF 攻击利用了浏览器会自动发送 Cookie 的特性。当用户登录到某个网站后,浏览器会保存该网站的 Cookie。当用户访问其他网站时,如果该网站构造了指向已登录网站的请求,浏览器会自动将 Cookie 发送给已登录网站。如果已登录网站没有对请求进行合适的验证,攻击者就可以冒充用户执行操作。
2.2 CSRF 攻击示例
假设有一个银行网站,用户可以通过以下 URL 转账:
/transfer?account=recipient&amount=100
攻击者可以构造一个包含以下代码的 HTML 页面:
<img src="/transfer?account=attacker&amount=1000000" width="0" height="0">
当用户访问这个页面时,浏览器会自动向银行网站发送转账请求,将 1000000 元转到攻击者的账户。如果银行网站没有对请求进行合适的验证,攻击者就可以成功地冒充用户转账。
2.3 CSRF 防御措施
-
使用 CSRF Token: 在每个表单中添加一个随机生成的 CSRF Token。 服务器在处理请求时,验证请求中是否包含正确的 CSRF Token。
-
CSRF Token 应该足够随机,并且对每个用户和每个会话都是唯一的。
-
CSRF Token 可以存储在 Cookie 中,也可以存储在 Session 中。
// 前端生成 CSRF Token function generateCSRFToken() { let token = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < 32; i++) { token += characters.charAt(Math.floor(Math.random() * characters.length)); } return token; } const csrfToken = generateCSRFToken(); localStorage.setItem('csrfToken', csrfToken); // 在表单中添加 CSRF Token const form = document.getElementById('myform'); const hiddenField = document.createElement('input'); hiddenField.type = 'hidden'; hiddenField.name = 'csrfToken'; hiddenField.value = csrfToken; form.appendChild(hiddenField); // 后端验证 CSRF Token (示例,具体实现取决于后端框架) function verifyCSRFToken(requestToken, storedToken) { return requestToken === storedToken; }
-
-
使用 SameSite Cookie: SameSite Cookie 可以限制 Cookie 的跨域访问。
-
SameSite=Strict
: Cookie 只能在同站点请求中使用。 -
SameSite=Lax
: Cookie 可以在同站点请求中使用,也可以在 GET 请求中使用。 -
SameSite=None
: Cookie 可以在跨站点请求中使用。 需要同时设置Secure
属性,表示 Cookie 只能通过 HTTPS 连接发送。
// 在服务器端设置 SameSite Cookie document.cookie = "sessionId=123456789; SameSite=Strict; Secure"; // 最安全的设置 // 或者 document.cookie = "sessionId=123456789; SameSite=Lax"; // 允许 GET 请求携带 Cookie // 需要HTTPS document.cookie = "sessionId=123456789; SameSite=None; Secure"; // 允许跨域,但必须使用 HTTPS
-
-
验证 HTTP Referer 头部: HTTP Referer 头部包含了请求的来源 URL。 服务器可以验证 Referer 头部,确保请求来自可信的来源。 但是,Referer 头部可以被篡改,因此不能完全依赖它。
-
双重 Cookie 验证 (Double Submit Cookie): 将 CSRF Token 同时存储在 Cookie 和表单中。 服务器在处理请求时,验证 Cookie 中的 CSRF Token 和表单中的 CSRF Token 是否一致。
// 前端 function setCookie(name, value, days) { let expires = ""; if (days) { let date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } document.cookie = name + "=" + (value || "") + expires + "; path=/"; } function getCookie(name) { let nameEQ = name + "="; let ca = document.cookie.split(';'); for (let i = 0; i < ca.length; i++) { let c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); } return null; } const csrfToken = generateCSRFToken(); setCookie('csrfToken', csrfToken, 7); // 设置 Cookie localStorage.setItem('csrfToken', csrfToken); // 在表单中添加 CSRF Token const form = document.getElementById('myform'); const hiddenField = document.createElement('input'); hiddenField.type = 'hidden'; hiddenField.name = 'csrfToken'; hiddenField.value = csrfToken; form.appendChild(hiddenField); // 后端 function verifyDoubleSubmitCookie(cookieToken, formToken) { return cookieToken === formToken && cookieToken !== null && formToken !== null; } // 示例 const cookieToken = getCookie('csrfToken'); const formToken = request.body.csrfToken; // 假设从请求体中获取 if (verifyDoubleSubmitCookie(cookieToken, formToken)) { // 处理请求 console.log('CSRF 验证通过'); } else { // 拒绝请求 console.log('CSRF 验证失败'); }
-
避免使用 GET 请求执行敏感操作: GET 请求容易被 CSRF 攻击,因为攻击者可以通过构造
<img>
标签或<a>
标签来发起 GET 请求。 应该使用 POST 请求来执行敏感操作。 -
用户教育: 教育用户不要轻易点击不明链接,不要访问不信任的网站。
三、总结
攻击类型 | 原理 | 防御措施 |
---|---|---|
XSS | 将恶意 JavaScript 代码注入到其他用户的浏览器中。 | 输入验证和过滤,输出编码,使用 CSP,使用安全的 JavaScript 框架和库,设置 HttpOnly Cookie,使用 SRI,避免使用 eval() ,避免直接操作 DOM,小心处理 URL 参数。 |
CSRF | 冒充用户发起恶意请求。 | 使用 CSRF Token,使用 SameSite Cookie,验证 HTTP Referer 头部,双重 Cookie 验证,避免使用 GET 请求执行敏感操作,用户教育。 |
四、提升安全意识,构建更安全的应用
XSS 和 CSRF 都是非常常见的 Web 安全威胁。 开发者需要充分了解它们的原理,并采取合适的防御措施,才能保护用户的数据安全。 加强安全意识,定期进行安全测试,及时修复漏洞,是构建更安全 Web 应用的关键。