AI 生成的网页为什么可能存在 XSS 漏洞?从 innerHTML 到 textContent 的安全讨论
前言
现在使用 AI 编程助手开发网页已经非常方便。
例如,只需要输入一句话:
“帮我开发一个待办事项工具,支持输入任务名称并添加到列表。”
AI 就可以快速生成 HTML、CSS 和 JavaScript。
但有一个细节很容易被忽略。
AI 生成的代码中,可能会出现这样的写法:
list.innerHTML += `<li>${taskName}</li>`;
表面上看,这段代码很简单,也能正常显示任务。
但是,如果 taskName 来自用户输入,并且没有进行安全处理,就可能产生跨站脚本攻击风险。
这类问题通常称为 XSS(Cross-Site Scripting,跨站脚本攻击)。
本文通过一个简单的网页案例,介绍 XSS 产生的原因,以及如何使用 JavaScript 原生 API 编写更安全的 DOM 更新代码。
一、什么是 XSS?
XSS 是一种常见的 Web 安全漏洞。
它通常发生在应用程序将不可信内容当成 HTML 或脚本交给浏览器解析时。
例如,一个网页允许用户填写评论:
这个工具很好用!
正常情况下,网页应该把这段内容当作普通文本显示。
但如果用户提交的不是普通文字,而是包含 HTML 标签、事件处理属性等内容的字符串,程序又直接使用 innerHTML 插入,就可能使浏览器解析出开发者没有预期的元素或行为。
XSS 可能造成的影响包括:
- 修改网页显示内容。
- 冒充正常页面诱导用户操作。
- 在受害者浏览器中执行未经授权的脚本。
- 在权限允许的情况下访问页面中的敏感数据或以用户身份发起操作。
需要注意,XSS 不代表攻击者一定能够读取所有 Cookie。
例如,设置了 HttpOnly 的 Cookie 不能通过普通 JavaScript 直接读取,但 XSS 仍可能造成其他安全风险。
二、为什么 innerHTML 容易产生问题?
来看一个简单例子:
<div id="result"></div>
JavaScript:
const result = document.getElementById("result");
const userInput = "<strong>你好</strong>";
result.innerHTML = userInput;
运行后,浏览器会解析字符串中的 HTML 标签。
最终显示的“你好”会变成粗体。
这就是 innerHTML 的特点:
它不是单纯显示字符串,而是会把字符串作为 HTML 片段进行解析。
如果字符串完全由开发者控制,且不存在不可信内容,使用 innerHTML 并不必然造成漏洞。
问题在于,当不可信输入参与 HTML 拼接时,浏览器可能创建开发者原本不想允许的元素和属性。
因此,不能把 innerHTML 当作普通文本显示接口使用。
三、textContent 为什么更适合显示用户输入?
对于只需要显示文字的场景,可以使用:
element.textContent
例如:
const result = document.getElementById("result");
const userInput = "<strong>你好</strong>";
result.textContent = userInput;
这时,页面会直接显示:
<strong>你好</strong>
浏览器不会把它解析成粗体标签。
原因在于:
textContent 将输入作为文本内容处理,而不是作为 HTML 标记解析。
对于评论、任务名称、搜索关键词、用户昵称等纯文本字段,这通常是更安全、更符合预期的选择。
当然,textContent 不是可以解决所有前端安全问题的万能方案。它主要解决的是将字符串作为文本插入 DOM 时的 HTML 解析风险。
四、实战:修复一个 AI 生成的待办列表
假设我们让 AI 开发一个简单的待办事项网页。
页面允许用户输入任务名称,然后添加到列表。
一种不安全的实现可能是:
function addTask(taskName) {
const list = document.getElementById("taskList");
list.innerHTML += `<li>${taskName}</li>`;
}
这里存在两个问题。
第一,用户输入被直接拼接到 HTML 字符串中。
第二,innerHTML += 会重新解析并替换列表中的部分 DOM 内容,可能导致原有元素状态或事件监听器受到影响。
更合理的方式是创建真实的 DOM 元素。
function addTask(taskName) {
const list = document.getElementById("taskList");
const item = document.createElement("li");
item.textContent = taskName;
list.appendChild(item);
}
这段代码的关键区别是:
createElement()创建明确的 HTML 元素。textContent设置普通文本。appendChild()将元素加入列表。
这样就不需要把用户输入作为 HTML 代码解析。
五、完整示例:一个安全的待办事项小工具
下面使用原生 HTML 和 JavaScript 实现一个简单示例。
新建 index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1"
>
<title>安全待办列表</title>
<style>
body {
max-width: 600px;
margin: 40px auto;
padding: 20px;
font-family: system-ui, sans-serif;
line-height: 1.6;
}
input {
padding: 10px;
width: 65%;
box-sizing: border-box;
}
button {
padding: 10px 16px;
cursor: pointer;
}
li {
padding: 8px 0;
overflow-wrap: anywhere;
}
</style>
</head>
<body>
<h1>安全待办列表</h1>
<form id="taskForm">
<label for="taskInput">任务名称</label>
<div>
<input
id="taskInput"
maxlength="100"
required
placeholder="请输入任务"
>
<button type="submit">添加</button>
</div>
</form>
<ul id="taskList"></ul>
<script>
const form = document.getElementById("taskForm");
const input = document.getElementById("taskInput");
const list = document.getElementById("taskList");
form.addEventListener("submit", (event) => {
event.preventDefault();
const taskName = input.value.trim();
if (!taskName) {
return;
}
const item = document.createElement("li");
item.textContent = taskName;
list.appendChild(item);
input.value = "";
input.focus();
});
</script>
</body>
</html>
这个页面不需要后端,也不需要第三方 JavaScript 库。
直接使用浏览器打开就可以运行。
如何测试?
首先输入正常任务:
学习 JavaScript
页面应正常添加一条任务。
然后输入包含 HTML 标记的内容:
<strong>测试任务</strong>
如果使用的是上面的安全实现,网页应该将完整字符串显示为普通文本,而不是把“测试任务”渲染为粗体。
还可以输入:
<img src="invalid-image" onerror="alert(1)">
在这个示例中,它应该被显示为文本,不应该创建真正的图片元素,也不应该触发其中的事件处理代码。
这一测试可以帮助发现是否存在把输入错误地当作 HTML 解析的问题。
不过,单次测试通过不代表整个应用不存在 XSS。实际项目中还要检查其他 DOM 更新位置和数据流。
六、如果业务确实需要显示 HTML 怎么办?
有些项目需要展示富文本。
例如:
- 博客文章。
- Markdown 转换后的内容。
- 用户评论中的有限格式。
- 带有链接和代码块的文档。
这时,单纯使用 textContent 无法满足格式展示要求。
但也不能直接把所有用户内容交给 innerHTML。
更合适的方法是使用经过安全处理的 HTML,并限制允许的元素和属性。
例如,只允许:
p、strong、em、ul、li、a
而不允许任意脚本、事件处理属性或危险 URL。
对于实际项目,可以考虑使用经过维护的 HTML 清理库,例如 DOMPurify,并按具体使用场景配置允许的标签和属性。
需要注意:
清理 HTML 不能简单依靠正则表达式删除 script 标签。
因为潜在危险内容不只存在于 <script> 标签中,还可能出现在事件属性、URL 及其他 HTML 解析上下文中。
如果开发的是纯文本工具,通常没有必要引入富文本解析功能,直接使用 textContent 更简单。
七、另一个容易忽略的风险:动态链接
有些 AI 生成的网页允许用户输入 URL,然后创建链接。
例如:
const link = document.createElement("a");
link.textContent = "点击查看";
link.href = userInput;
这里虽然使用了 textContent,但仍需要注意 href 的内容。
如果输入是不可信的 URL,就应该限制允许的协议。
例如,只允许 HTTP 和 HTTPS:
function getSafeUrl(input) {
try {
const url = new URL(input);
if (
url.protocol !== "https:" &&
url.protocol !== "http:"
) {
return null;
}
return url.href;
} catch {
return null;
}
}
使用:
const safeUrl = getSafeUrl(userInput);
if (safeUrl) {
link.href = safeUrl;
link.target = "_blank";
link.rel = "noopener noreferrer";
}
这说明:
即使没有使用 innerHTML,开发者仍然需要根据不同的输出位置进行安全处理。
文本内容、HTML 片段和 URL,并不是同一种安全上下文。
此外,允许 HTTP/HTTPS 并不意味着目标网站一定可信。对于存在钓鱼风险的应用,还可能需要进一步限制域名。
八、CSP 能否防止 XSS?
CSP 是 Content Security Policy 的缩写,通常翻译为内容安全策略。
它可以通过 HTTP 响应头限制网页允许加载和执行的资源。
例如,一个简化的策略可能是:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'
这类策略能够降低部分脚本注入带来的风险。
但是,CSP 不能代替正确的 DOM 操作和输入输出处理。
同时,CSP 的实际效果取决于部署方式及完整策略配置。
例如,上面的策略会阻止某些内联脚本执行,因此如果项目仍然使用内联 JavaScript,就需要调整代码结构或采用合适的 nonce、哈希等机制。
对于正式部署的应用,应当结合实际资源需求制定和测试 CSP,而不是直接复制一段配置就认为已经安全。
九、给 AI 编程助手增加哪些安全要求?
在使用 AI Shell、Codex 或其他 AI 编程工具开发网页时,可以把安全要求写进项目提示词。
例如:
所有用户输入、搜索关键词、任务名称和其他不可信字符串,默认使用 textContent 显示。禁止将未经安全处理的不可信内容直接拼接到 innerHTML、outerHTML 或 insertAdjacentHTML 中。动态 URL 必须进行协议校验。确需富文本时,应采用适当的 HTML 清理方案,并补充相关安全测试。
我认为这种要求比简单写一句“注意安全”更有效。
因为它明确指出了:
- 哪些内容是不可信数据。
- 哪些 DOM API 需要特别注意。
- 推荐使用什么替代方案。
- 如何通过测试验证。
对于 AI 自动生成的网站,还可以增加一项验收测试:
使用包含 HTML 标记和事件处理属性的字符串作为输入,确认其不会被意外解析或执行。
十、总结
AI 编程助手可以大幅减少前端代码编写时间,但快速生成代码并不代表代码天然安全。
尤其是涉及用户输入和动态网页内容时,开发者需要理解不同 DOM API 的行为区别。
通过本文可以总结出几个关键原则:
innerHTML会解析 HTML,不适合直接插入不可信字符串。textContent更适合显示普通文本。createElement()可以帮助我们明确构建页面结构。- 动态链接需要进行 URL 协议校验。
- 富文本需要使用适当的 HTML 清理方案。
- CSP 可以提供额外防护,但不能替代安全编码。
- AI 生成的网页应该包含必要的安全测试。
对于简单的网页项目,我更倾向于采用一个原则:
能用文本 API 解决的问题,就不要让浏览器把用户输入当作 HTML 解析。
这样不仅可以减少安全风险,也能够让代码更容易理解、测试和维护。
- 点赞
- 收藏
- 关注作者
评论(0)