Flask/Django中的XSS

学习笔记

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>:

xss_1_1

这里直接利用f-string 进行字符串插值,user_input 的值在插值时直接进入了最终的 HTML 输出

简单说就是用户的输入被当成合法的 HTML 和 JavaScript执行导致了未预期的行为

解决方法就是利用jinja2模板渲染用户的输入:

1
2
3
4
5
6
#在 Flask 中,除非显式指明不转义,Jinja2会自动转义所有值。这样可以排除所有模板导致的 XSS 问题
@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>

xss_1_2

看源码:

xss_1_3

可以看到<, >, '都被转义成了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>

那么如何显式指明不转义呢?可以通过|safeMarkup实现:

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)

#href_test.html:
<!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)

xss_1_4

为什么会这样?

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主要损害有两种:

  1. 拿cookie/csrf token
  2. 通过XSS进行某些重要操作(打API)

拿cookie比较简单,就是利用JS中访问当前页面的API document.cookie:

xss_1_5

对于这种攻击其实在设置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);

xss_1_6

第二种就是打特定的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,都會看到瀏覽器報錯。


Flask/Django中的XSS
http://example.com/2024/09/01/XSS-in-FlaskDjango/
作者
notbad3
发布于
2024年9月1日
许可协议