学习笔记
0x01 示例
Flask
:
1 2 3 4 5 6 7 8 9 10 11 12
| from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route('/') def index(): user_input = request.args.get('name', 'Guest') html = f"<h1>Hello, {user_input}!</h1>" return render_template_string(html) if __name__ == '__main__': app.run(debug=True)
|
令http://127.0.0.1:5000/?name=<script>alert('XSS')</script>
:
这里直接利用f-string 进行字符串插值,user_input 的值在插值时直接进入了最终的 HTML 输出
简单说就是用户的输入被当成合法的 HTML 和 JavaScript执行导致了未预期的行为
解决方法就是利用jinja2
模板渲染用户的输入:
1 2 3 4 5 6
| @app.route('/') def index(): user_input = request.args.get('name', 'Guest') html = "<h1>Hello, {{ user_input }}!</h1>" return render_template_string(html, user_input=user_input)
|
这时令http://127.0.0.1:5000/?name=<script>alert('XSS')</script>
:
看源码:
可以看到<
, >
, '
都被转义成了HTML实体(不是HTML语法的一部分),浏览器不会将这些字符当作 HTML 或 JavaScript 来执行
自己定义转义也可以,不过要注意的是不光标签要做转义,单引号和双引号同样要做转义,若只转移了标签符号,确实没办法直接插入标签,不过对于:
1
| <img src="<?= avatar_url ?>" alt="<?= nickname ?>" /><div><?= nickname ?></div>
|
令nickname = " onload="alert(1)
去闭合alt
标签,创造一个onload
属性(onload
属性属于html
属性):
<img src="avatar_url" alt="" onload="alert(1)" /><div>" onload="alert(1)</div>
那么如何显式指明不转义呢?可以通过|safe
或Markup
实现:
1 2
| #存在xss.html,render_template('xss.html', name=name) <h2>Hello, {{ name |safe }}</h2>
|
或:
1
| name = Markup(request.args.get('name'))
|
有一类 XSS
问题 Jinja
的转义无法阻止。 a
标记的 href
属性可以包含一个 javascript: URI
。如果没有正确保护,那么当点击它时浏览器将执行其代码。比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @app.route('/') def index(): user_input = request.args.get('name', 'https://www.baidu.com') return render_template('href_test.html', user_input=user_input)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <a href="{{ user_input }}">click here</a> </head> <body>
</body> </html>
|
name=javascript:alert(1)
为什么会这样?
javascript: alert('1')
是一种称为 JavaScript URL 的伪协议。这种 URL 以 javascript:
作为前缀,后面跟随一段 JavaScript
代码。点击链接时,浏览器会执行这段代码,而不是导航到一个新的页面
防御很简单,比如直接检测用户输入是不是http
或者https
开头就行:
1 2 3 4 5 6 7 8 9
| @app.route('/') def index(): white_list_href = ['http://', 'https://'] user_input = request.args.get('name', 'https://www.baidu.com') for href in white_list_href: if user_input.startswith(href): return render_template('href_test.html', user_input=user_input) else: return render_template('404.html')
|
或者设置CSP
。这东西其实就是告诉浏览器哪些来源的资源可以被载入,哪些不行。这个在第四部分详细记录。
0x03 危害
个人认为XSS主要损害有两种:
- 拿cookie/csrf token
- 通过XSS进行某些重要操作(打API)
拿cookie比较简单,就是利用JS
中访问当前页面的API
document.cookie
:
对于这种攻击其实在设置cookie的时候令HttpOnly=True
(Cookie 只能通过服务器端访问,不能通过 JavaScript 读取)即可:
1
| response.set_cookie('session_id', 'admin',secure=True,httponly=True,samesite='None')
|
也能拿csrf token,比如在一个Django项目中表单存在如下字段:
1
| input type="hidden" name="csrfmiddlewaretoken" value="AJTIijF8AHYNngTRCMDlAMswYAvmrFvIUxAL9RzsveUVsJJk7OgcAptN3aHdJtqY">
|
如何拿到这个隐藏字段的值呢?可以利用:
alert(document.querySelector('input[name="csrfmiddlewaretoken"]').value);
第二种就是打特定的API,比如执行如下js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <script> window.onload = function() { var xhr = new XMLHttpRequest(); xhr.open('GET', '/change_cookie', true); xhr.withCredentials = true; xhr.onload = function() { if (xhr.status === 200) { console.log('Response from /change_cookie:', xhr.responseText); } else { console.error('Failed to fetch /change_cookie:', xhr.statusText); } }; xhr.send(); }; </script>
|
0x04 利用CSP防护XSS
CSP 通过定义允许加载的资源(如脚本、样式、图片、框架等)的来源,限制浏览器可以执行哪些资源
舉例來說,如果我很確定網站上的 JS 都來自於同一個 origin,那我的 CSP 就可以這樣寫:
1 2 3 4 5 6 7
| Content-Security-Policy: default-src 'self'; script-src 'self' # default-src 是 CSP 的一种指令,用于指定默认资源加载的来源,包括脚本、样式、图片、字体、Ajax 请求等。 'self' 表示只允许从与当前网页相同的来源(即相同的域名、协议和端口)加载资源。 # script-src 是 CSP 的另一指令,专门用于控制 JavaScript 资源的加载。 'self' 同样表示只能从当前网页的域名加载脚本文件。
|
self
代表的是 same origin 的意思。這樣寫的話,如果你試著載入不是當前 origin 的 JS,或者是直接在頁面上用 inline 的方式執行 script,都會看到瀏覽器報錯。