文章目录
- 前言
- 1 新标签的XSS
-
- 什么是XSS?
- 风险场景:新标签如何被利用?
- 为什么会有这样的风险?
- 章节总结
- 2 iframe 的 sandbox 属性
-
- 什么是iframe?
- sandbox属性:给iframe加个“安全罩”
- 最危险的组合:allow-scripts + allow-same-origin
- 举个例子:正确使用sandbox
- 章节总结
- 3 noreferrer 属性
-
- 什么是Referer?
- noreferrer:隐藏你的“来源”
- 注意:noreferrer和noopener的区别
- 章节总结
- 4 postMessage:跨窗口通信的“安全绳”
-
- 什么是postMessage?
- postMessage的安全风险
-
- 风险1:不验证接收窗口的来源
- 风险2:消息内容未过滤,导致XSS
- 如何安全使用postMessage?
- 章节总结
- 5 WebStorage:本地存储的“安全坑”
-
- 什么是WebStorage?
- WebStorage的使用方法
- WebStorage的安全风险
- 举个危险的例子
- 章节总结
- 6 使用postMessage实现跨域:目标源不能“偷懒”
-
- 跨域通信的常见需求
- 错误用法:图省事用“*”
- 正确用法:明确指定目标源
- 为什么目标源必须严格指定?
- 章节总结
- 7 接受message:必须校验数据来源
-
- 接收消息的“信任危机”
- 错误用法:来者不拒
- 正确用法:先验明正身
- 额外提醒:校验消息内容格式
- 章节总结
- 8 禁止使用Web SQL数据库
-
- 什么是Web SQL数据库?
- Web SQL数据库的安全风险
- 如何检测并避免使用?
- 章节总结
- 9 钓鱼风险:window.open的“背后一刀”
-
- 什么是钓鱼攻击?
- window.open的风险场景
- 如何防御钓鱼风险?
- 额外提醒:谨慎打开不可信链接
- 章节总结
- 全文总结
前言
随着互联网的飞速发展,HTML5作为构建Web页面的核心技术,为我们带来了更丰富的功能和更流畅的用户体验——从视频播放、跨域通信到本地数据存储,HTML5的新特性让网页不再局限于简单的文字和图片展示。但这些强大的功能在便利我们的同时,也埋下了不少安全隐患。
HTML5的安全风险就藏在日常开发的细节里:一个未加限制的iframe、一段没做校验的跨域消息、一个随意使用的本地存储……稍有不慎,就可能被攻击者利用,导致用户信息泄露、网页被篡改甚至服务器被攻击。
本文专为安全小白打造,将用通俗易懂的语言拆解HTML5中最常见的9类安全风险,包括跨站脚本攻击(XSS)、iframe滥用、隐私泄露、钓鱼攻击等,并附上具体的防御方法。无论你是刚入门的开发者,还是对Web安全感兴趣的新手,都能通过本文快速掌握HTML5安全的核心要点,避开常见“坑点”,让你的网页更安全。
1 新标签的XSS
什么是XSS?
在讲HTML5新标签的安全风险前,我们先搞懂一个基础概念:XSS(Cross-Site Scripting,跨站脚本攻击)。简单来说,XSS就是攻击者在网页中插入恶意的脚本代码(比如JavaScript),当用户访问这个网页时,脚本会自动执行,可能会窃取用户的账号密码、Cookie,甚至控制用户的浏览器。
HTML5新增了<video>(视频播放)和<audio>(音频播放)等标签,让网页能直接嵌入音视频内容,但如果使用不当,这些标签也可能成为XSS的“温床”。
风险场景:新标签如何被利用?
<video>和<audio>标签需要通过src属性加载远程或本地的音视频资源,同时它们还支持很多“事件属性”——比如视频加载完成时触发的onloadedmetadata、播放进度变化时触发的ontimeupdate等。如果这些事件属性被插入了恶意脚本,就会导致XSS攻击。
举几个例子:
<!– video标签的事件属性被注入恶意脚本 –>
<video src="http://xxx.com/video.mp4" onloadedmetadata="alert('你的信息被窃取了');" tabindex="0"></video>
当视频加载完成后,onloadedmetadata里的alert脚本会自动执行,这只是一个简单的弹窗演示,实际攻击中可能会换成窃取Cookie的代码。
再比如音频标签:
<!– audio标签加载失败时触发恶意脚本 –>
<audio src="无效的地址" onerror="alert('XSS攻击成功')">
当src指向的地址无效时,浏览器会触发onerror事件,此时恶意脚本就会执行。
还有<video>标签里的<source>子标签(用于指定视频源),也可能被利用:
<video><source onerror="alert('XSS')"></video>
如果<source>加载失败,onerror里的脚本就会运行。
为什么会有这样的风险?
本质上,是因为开发者没有对标签的属性进行严格过滤。比如,当网页允许用户自定义音视频的地址或属性时,如果直接将用户输入的内容拼接到标签中,攻击者就可以趁机插入恶意的事件属性和脚本。
章节总结
HTML5的<video>和<audio>标签虽然方便了音视频展示,但它们的事件属性(如onloadedmetadata、onerror)可能成为XSS攻击的入口。防御的核心在于:
2 iframe 的 sandbox 属性
什么是iframe?
iframe是HTML中一个常用的标签,简单说就是“网页中的网页”——它可以在当前页面中嵌入另一个网页。比如很多网站的“登录框”“广告弹窗”,可能就是用iframe嵌入的其他页面。
但如果嵌入的页面是不可信的(比如第三方网站的内容),就可能有风险:比如对方的页面里有恶意脚本,可能会窃取当前页面的信息,或者篡改页面内容。这时候,sandbox属性就派上用场了。
sandbox属性:给iframe加个“安全罩”
sandbox是iframe的一个安全属性,就像给嵌入的页面加了一个“隔离罩”,可以限制它的功能,防止它搞破坏。你可以通过设置不同的参数,决定允许或禁止嵌入页面做哪些事。
下面是几个常用的sandbox参数,用通俗的话解释一下:
- allow-top-navigation:允许嵌入的页面“跳转到顶层页面”。比如嵌入的页面有个按钮,点击后让整个浏览器跳转到另一个网址,这个参数就控制是否允许这种操作;
- allow-scripts:允许嵌入的页面运行JavaScript脚本。如果不设置这个参数,嵌入页面里的所有脚本都会被禁用;
- allow-same-origin:允许嵌入的页面和当前页面“视为同源”。什么是“同源”?简单说就是“域名、协议、端口都一样”。同源的页面可以互相访问数据,不同源的则被限制。如果设置了这个参数,嵌入页面就可以和当前页面共享数据了;
- allow-forms:允许嵌入的页面提交表单。比如嵌入的页面有个注册表单,这个参数控制是否允许用户提交;
- allow-popups:允许嵌入的页面弹出新窗口(比如window.open打开的窗口);
- allow-popups-to-escape-sandbox:允许嵌入页面弹出的新窗口“脱离隔离罩”。也就是说,弹出的窗口不受当前sandbox的限制,可能拥有更多权限。
最危险的组合:allow-scripts + allow-same-origin
这里要划重点:强烈建议不要同时使用allow-scripts和allow-same-origin。
为什么?如果同时允许嵌入页面运行脚本(allow-scripts),又让它和当前页面同源(allow-same-origin),就相当于给了嵌入页面“既能执行代码,又能访问当前页面数据”的权限。如果嵌入的页面是不可信的,攻击者就可以通过脚本窃取当前页面的Cookie、LocalStorage,甚至修改页面内容,风险极大。
举个例子:正确使用sandbox
假设我们要嵌入一个第三方的广告页面,我们只希望它能显示内容和提交表单,不希望它运行脚本或跳转页面,那么可以这样设置:
<iframe src="https://第三方广告.com" sandbox="allow-forms"></iframe>
这里只给了allow-forms权限,其他功能(如脚本、跳转)都被禁用,大大降低了风险。
章节总结
iframe的sandbox属性是防御嵌入页面风险的重要工具,它通过限制嵌入页面的权限,防止恶意行为。使用时要注意:
3 noreferrer 属性
什么是Referer?
在讲noreferrer之前,先了解一个概念:Referer(请求来源)。当你点击网页中的链接(比如<a>标签)跳转到另一个页面时,浏览器会在请求头里加上Referer字段,告诉新页面“我是从哪个页面跳过来的”。
比如,你从https://a.com点击链接到https://b.com,b.com的服务器会收到一个Referer: https://a.com的请求头,这样b.com就知道用户来自a.com。
noreferrer:隐藏你的“来源”
noreferrer是<a>和<area>标签的一个属性,它的作用很简单:当用户点击带有这个属性的链接时,浏览器不会在请求头里发送Referer信息。
为什么需要隐藏来源?因为Referer可能包含敏感信息。比如,你在a.com的一个包含用户ID的页面(https://a.com/user?ID=12345)点击了一个到b.com的链接,如果不隐藏Referer,b.com就会知道用户的ID是12345,这可能泄露用户隐私。
使用方法也很简单,在<a>标签里加noreferrer即可:
<a href="https://b.com" noreferrer>点击跳转到b.com</a>
这样,跳转时b.com就不会收到Referer信息了。
注意:noreferrer和noopener的区别
可能有小白会混淆noreferrer和noopener(另一个属性)。简单说:
- noreferrer:隐藏Referer,同时也会让新页面无法通过window.opener访问原页面(后面“钓鱼风险”部分会讲window.opener);
- noopener:只阻止新页面通过window.opener访问原页面,不影响Referer的发送。
如果既想隐藏来源,又想防止window.opener被滥用,用noreferrer更合适。
章节总结
noreferrer属性的核心作用是保护用户的来源隐私,防止敏感的Referer信息被第三方网站获取。在以下场景建议使用:
4 postMessage:跨窗口通信的“安全绳”
什么是postMessage?
在HTML5之前,由于“同源策略”(不同域名、协议或端口的页面不能互相访问数据),两个不同窗口(比如两个标签页、父页面和iframe)之间很难通信。而postMessage就是HTML5推出的跨窗口通信工具,它允许任何窗口向其他窗口发送文本消息,不管是否同源。
比如,a.com的页面可以用postMessage给b.com的页面发送消息,实现数据传递。这在需要跨域协作的场景(如iframe嵌入第三方登录页)中非常有用。
postMessage的安全风险
虽然postMessage解决了跨域通信的问题,但如果用不好,就会变成安全漏洞的入口。主要有两个风险点:
风险1:不验证接收窗口的来源
postMessage发送消息时,需要指定接收消息的窗口和“目标源”(即接收方的域名)。如果目标源设为"*"(表示任何域名都可以接收),那么恶意网站可能会伪造一个窗口接收你的消息,窃取敏感数据。
比如,错误的用法:
// 向任何窗口发送消息,危险!
otherWindow.postMessage("包含用户信息的消息", "*");
这里的"*"会让消息被所有窗口接收,一旦消息包含用户密码、Token等敏感信息,就会被攻击者窃取。
风险2:消息内容未过滤,导致XSS
当接收方收到消息后,如果直接把消息内容插入到页面中(比如用innerHTML显示),而消息里包含恶意脚本,就会触发XSS攻击。
比如,接收方的代码:
window.addEventListener("message", function(event) {
// 直接将消息插入页面,危险!
document.body.innerHTML = event.data;
});
如果攻击者发送的消息是<script>alert('窃取Cookie')</script>,这段脚本就会被执行。
如何安全使用postMessage?
针对以上风险,正确的做法是:
发送消息时,明确指定目标源(接收方的域名),不要用"*"。 正确示例:
// 只向https://trusted.com发送消息
otherWindow.postMessage("安全的消息", "https://trusted.com");
接收消息时,先验证发送方的来源(event.origin),确保是可信的域名。 正确示例:
function receiveMessage(event) {
// 只处理来自https://trusted.com的消息
if (event.origin !== "https://trusted.com") {
return; // 不是可信来源,直接忽略
}
// 处理消息(如果要插入页面,需先过滤脚本)
document.body.textContent = event.data; // 用textContent而非innerHTML,避免XSS
}
window.addEventListener("message", receiveMessage, false);
对消息内容进行过滤:如果需要将消息显示在页面中,尽量用textContent(只显示文本,不解析HTML),或者对消息中的<script>等危险标签进行过滤。
章节总结
postMessage是跨窗口通信的强大工具,但必须做好安全防护:
5 WebStorage:本地存储的“安全坑”
什么是WebStorage?
WebStorage是HTML5提供的本地存储技术,简单说就是让网页能在用户的浏览器里保存数据(比如用户偏好设置、登录状态等),不用每次都从服务器获取。它分为两种:
- Session Storage(会话存储):数据只在当前浏览器会话中有效,关闭浏览器(或标签页)后数据就会被删除;
- Local Storage(本地存储):数据会一直保存在浏览器里,即使关闭浏览器再打开,数据仍然存在(除非手动删除)。
WebStorage的使用方法
使用起来很简单,通过JavaScript就能操作:
// 存储数据到Session Storage
window.sessionStorage.setItem("username", "张三");
// 从Session Storage读取数据
let name = window.sessionStorage.getItem("username");
// 存储数据到Local Storage
window.localStorage.setItem("theme", "dark");
// 从Local Storage读取数据
let theme = window.localStorage.getItem("theme");
另外,Firefox浏览器曾经单独实现过globalStorage,可以按域名存储数据,用法如下(但现在已不推荐使用,了解即可):
// 给指定域名存储数据(仅Firefox)
window.globalStorage.namedItem("https://example.com").setItem("key", "value");
WebStorage的安全风险
WebStorage虽然方便,但有个大问题:它受“同源策略”限制,但数据是明文存储在本地的。
- 同源策略保护:只有同一个域名的页面才能访问对应的WebStorage数据(比如a.com的页面不能访问b.com的Local Storage),这能防止跨域窃取;
- 明文存储风险:数据以明文形式存在浏览器的本地文件中,如果用户的设备被物理访问(比如别人拿到你的电脑),或者浏览器被恶意软件入侵,这些数据可能被直接读取。
更重要的是:绝对不要用WebStorage存储敏感信息,比如用户密码、银行卡号、登录Token(可能用于自动登录)等。一旦这些信息被窃取,攻击者可能直接登录用户账号,造成损失。
举个危险的例子
假设某个网站为了“方便用户”,把登录密码存在了Local Storage里:
// 危险!绝对不要这么做!
window.localStorage.setItem("password", "user123456");
如果攻击者通过XSS攻击获取了这个数据,就能直接拿到用户的密码。
章节总结
WebStorage是本地存储的实用工具,但使用时必须牢记:
6 使用postMessage实现跨域:目标源不能“偷懒”
跨域通信的常见需求
在实际开发中,跨域通信很常见。比如,a.com的页面嵌入了b.com的iframe,两者需要交换数据(如用户登录状态),这时候就会用到postMessage。
错误用法:图省事用“*”
很多新手为了图方便,会把postMessage的目标源设为"*",表示“任何域名都可以接收这个消息”。比如:
// 错误示例:向所有域名发送消息
let iframeWindow = document.getElementById("myIframe").contentWindow;
iframeWindow.postMessage("用户信息:xxx", "*");
这种做法的风险在于:如果有恶意网站通过某种方式(比如伪造iframe)成为消息的接收方,就能轻松窃取消息中的数据。尤其是消息包含用户ID、权限信息等敏感内容时,后果不堪设想。
正确用法:明确指定目标源
正确的做法是,在发送消息时,明确指定接收方的域名(即目标源),只允许可信的域名接收消息。比如,a.com要向b.com的iframe发送消息,就应该这样写:
// 正确示例:只向https://b.com发送消息
let iframeWindow = document.getElementById("myIframe").contentWindow;
iframeWindow.postMessage("用户信息:xxx", "https://b.com");
这样,只有https://b.com的窗口能接收消息,其他域名的窗口即使收到消息,浏览器也会拒绝处理。
为什么目标源必须严格指定?
浏览器对postMessage的目标源有严格校验:如果发送方指定的目标源与接收方的实际域名不匹配,接收方会忽略这条消息。这就像寄快递时,你必须写对收件人的地址,否则快递会被退回。
如果用"*",就相当于“地址随便填”,任何收件人都能收到,显然不安全。
章节总结
用postMessage实现跨域通信时,目标源的设置是核心安全点:
7 接受message:必须校验数据来源
接收消息的“信任危机”
当一个页面通过addEventListener监听message事件时,它会收到来自各种窗口的消息——可能是可信的合作网站,也可能是恶意网站伪造的消息。如果不验证消息的来源,就可能被攻击者欺骗,执行危险操作。
错误用法:来者不拒
很多新手会直接写一个监听事件,不做任何来源校验,比如:
// 错误示例:接收所有消息,不校验来源
window.addEventListener("message", function(event) {
// 直接处理消息,比如执行脚本或修改页面
eval(event.data); // 极其危险!
}, false);
这种代码就像“不查身份证就开门”,如果攻击者发送一条包含恶意代码的消息(比如"document.location='https://evil.com'"),页面就会执行这条代码,导致跳转至恶意网站,或被窃取数据。
正确用法:先验明正身
正确的做法是,在处理消息前,先通过event.origin(发送消息的窗口的域名)校验来源,只处理来自可信域名的消息。比如:
// 正确示例:校验来源后再处理消息
function receiveMessage(event) {
// 只信任https://trusted.com发送的消息
if (event.origin !== "https://trusted.com") {
console.log("忽略来自未知来源的消息");
return; // 不是可信来源,直接返回
}
// 处理可信来源的消息(同时注意过滤消息内容,防止XSS)
if (event.data.type === "updateUser") {
updateUserInfo(event.data.content); // 安全处理
}
}
// 绑定监听事件,指定处理函数
window.addEventListener("message", receiveMessage, false);
这里的event.origin会返回发送方的域名(如"https://trusted.com"),我们可以通过它判断消息是否来自可信的合作方。
额外提醒:校验消息内容格式
除了校验来源,还建议校验消息的格式。比如,约定消息必须是包含type和content的对象,避免处理格式异常的消息:
function receiveMessage(event) {
if (event.origin !== "https://trusted.com") return;
// 校验消息格式
if (typeof event.data !== "object" || !event.data.type || !event.data.content) {
console.log("消息格式错误,忽略");
return;
}
// 按类型处理消息
switch(event.data.type) {
case "update":
handleUpdate(event.data.content);
break;
// 其他类型…
}
}
章节总结
接收message时,校验来源是防止恶意消息的关键:
8 禁止使用Web SQL数据库
什么是Web SQL数据库?
Web SQL数据库是HTML5早期提出的一种本地数据库解决方案,允许网页通过SQL语句在浏览器中存储和操作数据(类似MySQL)。它的使用方法是通过window.openDatabase创建或打开数据库:
// 创建或打开Web SQL数据库
let db = window.openDatabase("mydb", "1.0", "我的数据库", 2 * 1024 * 1024);
虽然听起来很方便,但实际上,Web SQL数据库已经被W3C(万维网联盟)废弃,因为它的规范不够统一,不同浏览器的实现差异很大。
Web SQL数据库的安全风险
除了兼容性问题,Web SQL数据库还有一个严重的安全隐患:只要知道数据库的名称,同一域名下的任何页面都能访问它。
比如,a.com的页面A创建了一个名为userdata的Web SQL数据库,存储了用户的隐私信息。那么a.com的另一个页面B,只要知道数据库名称是userdata,就能通过window.openDatabase打开并读取其中的数据。
更危险的是,如果a.com存在XSS漏洞,攻击者可以通过注入脚本,轻松获取数据库名称并访问其中的数据,导致信息泄露。
如何检测并避免使用?
如果你是开发者,检查代码中是否存在window.openDatabase——只要有这个函数调用,就说明使用了Web SQL数据库,存在风险。
替代方案:使用IndexedDB(另一种HTML5本地存储方案),它的安全性和规范性更好,且受同源策略保护,访问控制更严格。
章节总结
Web SQL数据库由于安全风险和兼容性问题,已被废弃,强烈建议禁止使用:
9 钓鱼风险:window.open的“背后一刀”
什么是钓鱼攻击?
钓鱼攻击是指攻击者伪造一个与合法网站(比如银行、购物平台)高度相似的页面,诱骗用户输入账号密码等敏感信息。而HTML5中的window.open函数(用于打开新窗口),如果使用不当,就可能成为钓鱼攻击的“帮凶”。
window.open的风险场景
假设你在A网站(a.com)点击了一个按钮,通过window.open打开了B网站(b.com)的页面:
// A网站的代码:打开B网站
window.open("https://b.com");
此时,B网站的页面可以通过window.opener访问A网站的窗口对象。如果B网站是恶意的,它可以通过window.opener.location修改A网站的地址,比如:
// B网站的恶意代码
window.opener.location = "https://fake-a.com"; // 把A网站跳转到伪造的页面
这时候,用户原本的A网站页面会被替换成一个和A网站长得一模一样的伪造页面(fake-a.com)。当用户以为还在A网站上操作时,输入的账号密码就会被攻击者窃取——这就是典型的“钓鱼”。
如何防御钓鱼风险?
防御的核心是切断B网站对A网站的控制,方法很简单:在使用window.open打开新窗口后,将window.opener设为null(空),这样新窗口就无法访问原窗口了。
正确的代码:
// 打开新窗口后,立即切断opener关联
let newWindow = window.open("https://b.com");
newWindow.opener = null;
这样,B网站的window.opener会变成null,无法再修改A网站的地址,避免了钓鱼风险。
额外提醒:谨慎打开不可信链接
即使做了opener = null的处理,也要尽量避免打开不可信的第三方链接。因为攻击者可能会在新窗口中直接展示伪造页面,诱骗用户操作(比如让用户“点击返回原网站”,实际跳转到钓鱼页)。
章节总结
window.open打开新窗口时,可能存在钓鱼风险,恶意网站可通过window.opener篡改原页面地址。防御方法:
全文总结
HTML5的新特性为Web开发带来了便利,但也伴随着多样的安全风险。对于安全小白来说,掌握这些风险的核心防御原则至关重要:
- 输入过滤:任何用户输入或第三方内容,都要经过过滤(如XSS防御);
- 权限最小化:如iframe的sandbox属性,只开启必需的权限;
- 来源校验:跨域通信(postMessage)时,严格验证发送方和接收方的域名;
- 敏感信息保护:不把密码、Token等存到WebStorage或Web SQL数据库;
- 切断不必要关联:如window.opener = null,防止钓鱼攻击。
安全防护没有“银弹”,但只要在开发中注意这些细节,就能大幅降低风险。希望本文能帮助你迈出HTML5安全的第一步,让你的网页既强大又安全。
本文是「Web安全基础」系列的第 11 篇,点击专栏导航查看全部系列内容。
评论前必须登录!
注册