跨站点脚本(XSS)是一种攻击,它允许一个站点中的JavaScript在另一站点上运行。 XSS之所以有趣,并不是因为攻击的技术难度,而是因为它利用了Web浏览器的某些核心安全机制,并且由于其无处不在。了解XSS及其缓解措施可提供有关Web如何工作以及站点如何安全(和不安全地)彼此隔离的大量见解。
最初,网络是静态HTML文档的集合,浏览器将呈现这些HTML文档供用户查看。随着网络的发展,对更丰富的文档的需求也在增长,这导致了JavaScript和cookie的发展:使文档具有交互性的JavaScript和允许浏览器保存文档状态的cookie。
这些功能的出现导致浏览器不仅呈现HTML文档,而且还提供文档的内存表示形式,称为文档对象模型(DOM),作为开发人员文档的API。 DOM为开发人员提供了文档HTML标签的基于树的表示形式,并且还可以访问cookie来检索文档的状态。随着时间的流逝,DOM从基本上是只读的结构变成了读写结构,在这种结构中,更新DOM将导致文档的重新呈现。
一旦文档具有执行代码的能力,浏览器就需要为JavaScript程序定义执行上下文。所开发的策略称为“同源策略”,仍然是浏览器安全性的基本安全性原语之一。最初,Same-Origin策略指出,一个文档中的JavaScript只能访问其自己的DOM以及具有相同来源的其他文档的DOM。后来,当添加XMLHttpRequest(和现在的Fetch)时,紧接着是同一原始策略的修改版本。这些API可以向任何来源发出请求,它们只能读取对来自相同来源的请求的响应。
原产地到底是什么?它是协议,主机名和文档端口的元组。
图1:此URL的方案,主机和端口是组成浏览器起源的元组。
图2:实际中的“同源源”示意图。在www.evil.com上运行的JavaScript无法访问www.example.com的DOM。
如图2所示,Same-Origin策略在缓解对静态站点的攻击方面非常有用。但是,对动态生成的接受用户输入的站点进行攻击非常困难,因为Web允许混合代码和数据。混合代码和数据使攻击者控制的输入可以在文档的原点内执行。
反射和存储的XSS攻击在本质上是相同的,因为它们都依赖于将恶意输入发送到后端服务器以及服务器(在某个时候)将输入呈现给用户。反射的XSS会立即发生,通常以攻击者恶意点击的链接的形式出现,然后受害者单击。当攻击者上传恶意输入并随后向用户显示时,就会发生存储的XSS。基于DOM的攻击的不同之处在于,它们仅在客户端发生,并且涉及操纵DOM的恶意输入。
如果您不完全掌握上面的表格,请放心,我们在下面介绍一些示例时,它们会更有意义。
在下面,您可以看到一个简单的基于Go的网络应用程序,该应用程序会将其输入(即使是恶意脚本)“反射”回用户。您可以通过以下方式运行该应用程序:将其保存在名为xss1.go的文件中,然后键入go runxss1.go。
包主要导入(" fmt"" log"" net / http")func handler(w http.ResponseWriter,r * http.Request){w。标头()。设置(" X-XSS-Protection&#34 ;," 0")消息,确定:= r。网址。 Query()[" message"]如果!好的{消息= []字符串{" hello,world"}} fmt。 Fprintf(w,"< p>%v< / p>< / html>&#34 ;, messages [0])} func main(){http。 HandleFunc(" /&#34 ;, handler)日志。致命的(http。ListenAndServe(" 127.0.0.1:8080&#34 ;, nil))}
图3:其中包含反射的XSS攻击的Web应用程序示例。
看一下源代码,您将看到服务器返回了一个文档,该文档看起来类似于图(4)中的文档。请注意,代码和数据的混合是如何使这种攻击发生的。
不可否认,由于XSS保护已被明确禁用,因此这似乎是一个有些人为的例子。但是,这种形式的XSS保护始终基于启发式,并针对不同的浏览器提供了多种解决方法。禁止创建简单的跨浏览器示例来说明XSS攻击的核心概念。此外,某些浏览器正在删除这些基于启发式的XSS保护,例如,如果您运行的是Chrome 78或更高版本,则无需包含w.Header()。Set(" X-XSS-Protection&# 34;," 0")行才能使此攻击起作用。
存储的XSS攻击从根本上类似于反射式攻击,主要区别在于攻击有效载荷来自数据存储,而不是直接来自输入。例如,攻击者会将有效负载上传到Web应用程序,然后将其显示给每个登录的用户。
以下是用Go语言编写的简单聊天应用程序,它说明了存储的XSS攻击。您可以通过以下方式运行该应用程序:将其保存在名为xss2.go的文件中,然后键入go runxss2.go。
包主要导入(" fmt"" log"" net / http"" strings"" sync")var db [ ]字符串var mu sync。互斥变量var tmpl =`< form action =" / save">消息:<输入名称="消息" type =" text"< br> <输入类型="提交"值="提交"> < / form> %v`func saveHandler(w http。ResponseWriter,r * http。Request){mu。 Lock()延迟亩。解锁()r。 ParseForm()消息,好的:= r。填写[" message"]如果!好的{http。错误(w,"缺少消息&#34 ;, 500)} db = append(db,messages [0])http。重定向(w,r," /&#34 ;, 301)} func viewHandler(w http。ResponseWriter,r * http。Request){w。标头()。设置(" X-XSS-Protection&#34 ;," 0")w。标头()。设置(" Content-Type&#34 ;," text / html; charset = utf-8")var sb字符串。建造者_,消息:= range db {sb。)的WriteString("< ul>")。 WriteString("< li>" +消息+"< / li>")} sb。 WriteString("< / ul>")fmt。 Fprintf(w,tmpl,sb。String())} func main(){http。 HandleFunc(" /&#34 ;, viewHandler)http。 HandleFunc(" / save&#34 ;, saveHandler)日志。致命的(http。ListenAndServe(" 127.0.0.1:8080&#34 ;, nil))}
图5:其中存储了XSS攻击的Web应用程序示例。
攻击分为两个阶段。首先,将攻击有效负载保存到storeHandler函数中的数据存储中。接下来,当页面在viewHandler中呈现时,攻击有效负载将直接添加到输出中。
再次,罪魁祸首是允许数据和代码混合。浏览器无法判断有效载荷是故意的还是恶意的。
基于DOM的XSS攻击不涉及后端,而仅发生在客户端。它们也很有趣,因为现代的Web应用程序正在将逻辑转移到客户端。当允许用户输入以不安全的方式直接操作DOM时,就会发生基于DOM的XSS攻击。对于攻击者而言,好消息是DOM有多种可被滥用的方式,其中最流行的是innerHTML和document.write。
以下是提供静态内容的Web应用程序的示例。它与所反映的XSS示例基本相同,但此处的攻击将完全在客户端进行。您可以通过将其保存在名为xss3.go的文件中,然后键入go run xss3.go来运行该应用程序。
包主要导入(" fmt"" log"" net / http")const content =`< html> < head> < script> window.onload = function(){var params = new URLSearchParams(window.location.search); p = document.getElementById(" content")p.innerHTML = params.get(" message")}; < / script> < / head> <身体> < p id =" content">< / p> < / body> < / html> `func handler(w http。ResponseWriter,r * http。Request){w。标头()。设置(" X-XSS-Protection&#34 ;," 0")fmt。 Fprintf(w,content)} func main(){http。 HandleFunc(" /&#34 ;, handler)日志。致命的(http。ListenAndServe(" 127.0.0.1:8080&#34 ;, nil))}
图6:其中包含基于DOM的XSS攻击的Web应用程序的示例。
要查看此攻击,请导航至http:// localhost:8080 /?message ="< img src = 1 onerror = alert(1); />"。请注意,攻击向量略有不同,并且XSS接收器innerHTML不会直接执行脚本。但是,它将添加HTML元素,然后将执行JavaScript。在给出的示例中,添加了一个图像元素,该图像元素在发生错误时执行JavaScript。由于攻击者总是方便地提供无效来源,因此总是会发生错误。
如果要直接添加脚本元素,则必须使用其他XSS接收器。如前所述,您很幸运,因为DOM提供了多种危险的接收器。将图(6)中的脚本元素替换为图(7)中的脚本元素,并导航到以下URL http:// localhost:8080 /?message ="< script> alert(1);< / script>"。发生这种攻击是因为document.write直接接受脚本元素。
尽管通常不称为XSS攻击,但是存在一些值得一提的相关途径。
一种相关的攻击途径是错误地设置了HTTP响应的Content-Type。无论是在后端级别(响应设置了错误的Content-Type标头),还是在浏览器尝试嗅探MIME类型时,都可能发生这种情况。 Internet Explorer尤其容易受此影响,经典示例是图像上传服务,攻击者改为使用JavaScript上传服务。浏览器看到Content-Type设置为image / jpg,但是有效负载包含JavaScript,并执行JavaScript将其转变为XSS攻击。
另一个相关的攻击途径是通过使用JavaScript方案的URL。例如,想象一个允许用户控制链接目标的网站,如图(8)所示。如果攻击者可以控制目标,则攻击者可以提供一个使用JavaScript方案执行JavaScript的URL。
要查看这种攻击的实际效果,您可以通过将应用程序保存在名为xss4.go的文件中,然后键入go run xss4.go来运行该应用程序。要查看XSS攻击,请导航至http:// localhost:8080?link = javascript:alert(1)。
包主要导入(" fmt"" log"" net / http")func handler(w http.ResponseWriter,r * http.Request){w。标头()。设置(" X-XSS-Protection&#34 ;," 0")链接,确定:= r。网址。 Query()[" link"]如果! ok {消息= []字符串{" example.com"}} fmt。 Fprintf(w,`< html>< p>< a href ="%v"> Next< / p>< / html>`,links [0])} func main( ){http。 HandleFunc(" /&#34 ;, handler)日志。致命的(http。ListenAndServe(" 127.0.0.1:8080&#34 ;, nil))}
不幸的是,没有XSS的单一缓解措施。如果确实如此,那么XSS就不会成为普遍存在的问题。从上一节可以看出,XSS的根本问题是由于代码和数据之间缺乏分隔而引起的。 XSS的缓解措施通常包括清理数据输入(以确保输入不包含任何代码),转义所有输出(以确保数据不以代码形式显示)以及重新构造应用程序,以便从定义明确的端点加载代码。
抵御XSS的第一道防线是输入清理。每当接受任何数据时,请确保数据格式符合您的期望。实际上,此操作将数据列入白名单,以确保应用程序不接受任何代码。
不幸的是,输入清理是一个难题。对于所有情况和所有应用程序,都没有通用的工具或技术。最好的选择是对应用程序进行结构设计,使其要求开发人员考虑接受的数据类型,并提供一个方便的位置,不仅可以放置消毒,而且可以预期进行消毒。
编写Go应用程序时,一个好的模式是不要在HTTP请求处理程序中包含任何应用程序逻辑,而应使用HTTP请求处理程序来解析和验证输入,然后将其发送给其他可处理的包(或结构)应用逻辑。请求处理程序不仅变得非常简单,而且还提供了一个方便的集中位置,可以在代码检查期间进行查看,以确保正确地清理了输入。
图(9)显示了我们如何重写saveHandler以限制仅应接受ASCII字符[A-Za-z \。]的工作。
func saveHandler(w http。ResponseWriter,r * http。Request){r。 ParseForm()消息,好的:= r。填写[" message"]如果!好的{http。 Error(w," missing message&#34 ;, 500)} re:= regexp。如果重新编译,则必须填写(`^ [A-Za-z \\。] + $`)。 Find([] byte(messages [0])))=="" {http。错误(w,"无效的消息&#34 ;, 500)} db。附加(邮件[0])http。重定向(w,r," /&#34 ;, 301)}
毕竟,尽管这看起来有些人为设计,但聊天应用程序通常必须接受的字符数量远远超过图(9)中的有限字符。但是,应该注意,应用程序接受的许多数据都是相当结构化的。地址,电话号码,邮政编码等都具有可以验证的固有结构。
下一道防线是输出转义。继续以聊天应用程序为例,它清楚地看到了XSS错误:HTML是手写的。对于聊天应用程序,将从数据库中提取的所有内容直接注入到输出文档中。
通过转义所有不安全的输出,可以使同一应用程序实质上更安全(即使已将代码注入其中)。实际上,这正是Go中html / template包所做的。使用模板语言和上下文相关的解析器在呈现数据之前转义数据,而不是手写输出文档,这将减少执行恶意数据的机会。
下面是使用html / template包的示例。您可以通过以下方式运行该应用程序:将其保存在名为xss5.go的文件中,然后键入go runxss5.go。
包主要导入(" bytes"" html / template"" io"" log"" net / http"&# 34; sync")var db []字符串var mu sync。互斥变量var tmpl =`< form action =" / save">消息:<输入名称="消息" type =" text"< br> <输入类型="提交"值="提交"> < / form> < ul> {{ 范围 。 }}< li> {{。 }}< / li> {{end}}< / ul>`func saveHandler(w http。ResponseWriter,r * http。Request){mu。 Lock()延迟亩。解锁()r。 ParseForm()消息,好的:= r。填写[" message"]如果!好的{http。错误(w,"缺少消息&#34 ;, 500)} db = append(db,messages [0])http。重定向(w,r," /&#34 ;, 301)} func viewHandler(w http。ResponseWriter,r * http。Request){w。标头()。设置(" X-XSS-Protection&#34 ;," 0")w。标头()。设置(" Content-Type&#34 ;," text / html; charset = utf-8")t:=模板。 New(" view")t,err:= t。如果err!= nil {http。 Error(w,err。Error(),500)返回} var buf字节。缓冲区err = t。如果err!= nil {http,则执行(& buf,db)。 Error(w,err。Error(),500)返回} io。复制(w,&buf)} func main(){http。 HandleFunc(" /&#34 ;, viewHandler)http。 HandleFunc(" / save&#34 ;, saveHandler)日志。致命的(http。ListenAndServe(" 127.0.0.1:8080&#34 ;, nil))}
通过导航到http:// localhost:8080并输入< script> alert(1);< / script&gt ;,尝试使用以前使用的XSS攻击。到输入框。请注意,未触发警报。
要查看发生了什么,请打开浏览器控制台并查看DOM中的li元素。有两个属性值得关注:innerHTML和innerText。
请注意,如何使用输出转义,我们能够干净地分离代码和数据,从而减轻XSS攻击?
内容安全策略(CSP)允许Web应用程序定义一组白名单源,以从中加载内容(例如来自的脚本)。通过拒绝内联脚本并仅从某些来源加载脚本,可以利用CSP分离代码和数据。
为小型自包含应用程序编写CSP很简单-从默认情况下拒绝所有源的策略开始,然后允许一小部分受信任的源。但是,为大型站点编写有效的CSP一直很困难。一旦网站开始从外部源加载内容(例如嵌入Tweet),CSP就会变得庞大而笨拙。一些开发人员完全放弃了包含unsafe-inline指令,从而完全违反了CSP的目的。
为了简化编写CSP的过程,CSP3引入了严格动态指令。每次维护页面时,应用程序都会生成一个随机数(即刻),而不是维护大量的受信任源白名单。该随机数与页面的标题一起发送,并嵌入到脚本标记中。这指示浏览器信任具有匹配随机数的脚本以及它们可能加载的任何脚本。这意味着不必将脚本列入白名单并尝试找出它们加载了哪些其他脚本,然后将其列入白名单,并一遍又一遍地执行此递归模式,而只需将要导入的顶级脚本列入白名单。
对于如何使用strict-dynamic指令编写有效的CSP,Google拥有出色的资源。使用Google建议的“严格CSP”方法,让我们看一个接受用户输入并嵌入推文的简单应用程序的外观。您可以通过将其保存在名为xss6.go的文件中,然后键入go run xss6.go来运行该应用程序。
包主要导入(" bytes"" crypto / rand"" encoding / base64"" fmt"" html / template" " log /#34;&net / http"" strings")const scriptContent =`document.addEventListener(' DOMContentLoaded&#39 ;, function(){var updateButton = document.getElementById(" textUpdate"); updateButton.addEventListener(" click&#34 ;, function(){var p = document.getElementById(" content"); var message = document.getElementById(" textInput")。value; p.innerHTML = message;});};`const htmlContent =`< html>< head>< script src =&#34 ; script.js" nonce =" {{。}}">< / script>< / head>身体< p id =" content&#34 ;< / p>< div class =" input-group mb-3">< input type =" text" class =" form-control& #34; id =" textInput">÷ div class =" input-group-append">< button class =" btn btn-outline-secondary&# 34 ;类型="按钮" id =" textUpdate"> Update< / button> < / div> < / div> < blockquote class =" twitter-tweet" data-lang = en"> < a href =" https://twitter.com/jack/status/20?ref_src = twsrc%5Etfw"> 2006年3月21日< / a> < / blockquote> <脚本异步src =" https://platform.twitter.com/widgets.js"随机数=" {{。 }}" charset =" utf-8"< / script> < / body> < / html> `func generateNonce()(字符串,错误){buf:= make([] byte,16)_,err:= rand。如果err!= nil {return"&#34 ;, err}返回base64,则读取(buf)。 StdEncoding。 EncodeToString(buf),nil}函数属
......