python安全问题
简介
这篇文章中,记录一些python 的安全问题
环境记录
具体demo代码见下文
- python2
- python3
- pyenv
从python开始
Python 是一个高层次的结合了解释性、编译性、互动性和面向对象的脚本语言。
Python 的设计具有很强的可读性,相比其他语言经常使用英文关键字,其他语言的一些标点符号,它具有比其他语言更有特色语法结构
- Python 是一种解释型语言: 这意味着开发过程中没有了编译这个环节。类似于PHP和Perl语言
- Python 是交互式语言: 这意味着,您可以在一个 Python 提示符 >>> 后直接执行代码
- Python 是面向对象语言: 这意味着Python支持面向对象的风格或代码封装在对象的编程技术
- Python 是应用广泛的语言:支持广泛的应用程序开发,从简单的文字处理到 WWW 浏览器再到游戏
安全问题
断言问题
在 Python 中,assert
语句可以用于调试时的检查,但在安全性方面存在一些隐患,特别是在生产环境中使用时。以下是常见的 assert
安全问题:
-
运行时被忽略:
-
assert
语句在 Python 中只在调试模式下有效。如果使用了优化模式 (python -O
) 运行 Python 代码,所有的assert
语句都会被跳过。这意味着在生产环境中,assert
可能不会被执行,从而跳过关键的检查,可能导致意外的安全漏洞。 - 优化模式会使代码运行更快且占用内存更少
-
-
不适用于异常处理:
-
assert
主要用于开发过程中的检查,不能替代正式的错误处理。依赖assert
来验证输入或执行关键逻辑检查是不安全的。在处理外部输入、权限检查或其他安全相关逻辑时,应该使用标准的异常处理机制(如if
语句和try-except
块)。
-
-
敏感信息泄露:
-
assert
语句中可能包含敏感信息,如内部状态、系统路径或错误消息。在调试模式下,这些信息可能会被暴露在日志或错误报告中,攻击者可以利用这些信息进行进一步攻击。
-
安全替代方案
-
使用条件和异常:
-
在关键代码路径中,使用显式的条件检查和异常处理,而不是
assert
。例如:if not condition: raise ValueError("Invalid condition")
-
-
日志记录与监控:
- 通过安全的日志记录机制代替
assert
语句,避免敏感信息暴露的同时提供足够的调试信息。
- 通过安全的日志记录机制代替
-
输入验证:
- 对外部输入进行严格的验证,避免依赖
assert
进行输入验证。
- 对外部输入进行严格的验证,避免依赖
总体来说,assert
更适合开发阶段调试,在生产环境下使用时,应该替换为更严谨的异常处理机制。
一个demo
def authenticate_user(user, password):
# 在生产环境中,assert 可能会被忽略,导致未进行身份验证检查
assert user == "admin" and password == "secure_password", "Authentication failed!"
print(f"Welcome, {user}!")
# 示例:错误的使用 assert 进行身份验证
user_input = input("Enter username: ")
password_input = input("Enter password: ")
authenticate_user(user_input, password_input)
创建文件夹
在 Python 中使用 os.makedirs()
或 os.mkdir()
创建目录时,如果不正确设置目录权限,可能导致一系列安全问题。尤其是在多用户系统或处理敏感数据时,默认或不安全的权限设置会让目录暴露于潜在的攻击风险中。
权限问题
- demo.py
import os
def create_dir(dir_path):
os.makedirs(dir_path, mode=0o700)
create_dir("a/b/c")
- 当python > 3.6 文件夹权限分别为
- a 0x755
- b 0x755
- c 0x700
- 当python < 3.6 (测试版本为2.7) 文件夹权限分别为
- aa 0x700
- b 0x700
- c 0x700
- 在 Python > 3.6 中,函数
os.makedirs
具有与 Linux 命令相同的属性:mkdir -m 700 -p A/B/C
条件竞争
在某些情况下,目录创建时没有立即设置权限,后续通过 os.chmod()
修改权限。这会产生一个时间窗口,在这个窗口内,存在潜在的风险利用可能
import os
dir_path = "/tmp/sensitive_data"
os.makedirs(dir_path)
xxxxx
xxxxx
os.chmod(dir_path, 0o700) # 事后设置权限
不安全的路径
如果创建目录时没有正确验证路径,攻击者可以通过传入恶意路径(如 ../../
),在不受控的路径下创建目录,可能导致系统被破坏或数据被篡改
import os
def create_user_dir(username):
dir_path = f"/home/{username}/data"
os.makedirs(dir_path)
# 如果 username 是 "../../etc", 那么目录将被创建在 /etc 下
create_user_dir("../../etc")
路径拼接
主要为 os.path.join
函数的不规范使用
路径遍历攻击
如果用户提供的输入包含恶意构造的路径,比如通过 ../
(父目录路径),攻击者可以使用这种输入绕过目录限制,访问系统中其他敏感目录。
- demo.py
import os
def save_file(upload_dir, filename):
# 使用 os.path.join 拼接路径
file_path = os.path.join(upload_dir, filename)
with open(file_path, 'w') as f:
f.write("This is a test file.")
# 假设上传目录是 /uploads
save_file("/uploads", "../../etc/passwd")
符号链接攻击(Symlink Attack)
攻击者可能创建符号链接指向敏感的系统文件,而应用程序可能错误地信任这些符号链接,并在这些路径下写入或读取数据,从而导致安全问题(这部分攻击场景相对比较理想化,只提供思路)
绝对路径覆盖
如果输入的路径是一个绝对路径(以 /
开头),os.path.join()
会忽略前面的路径部分,直接返回这个绝对路径。攻击者可以利用这种行为覆盖指定的文件路径
import os
# 如果 filename 是一个绝对路径
file_path = os.path.join("/safe/directory", "/etc/passwd")
print(file_path) # 输出: /etc/passwd
临时文件问题
tempfile.NamedTemporaryFile
函数用于创建具有特定名称的临时文件。其中, prefix
和suffix
参数容易受到路径遍历攻击
import tempfile
import os
id = "/../tmp/test"
tmp_file = tempfile.NamedTemporaryFile(prefix=id, delete=False)
file_name = tmp_file.name
tmp_file.close()
用户输入的id
用作临时文件的前缀。如果攻击者将有效负载/../var/www/test
作为id
参数传递,则会创建以下tmp文件: /var/www/test_zdllj17
。可能存在风险较高的组合漏洞
Extended Zip Slip
Extended Zip Slip 是一种路径遍历攻击,涉及将压缩文件(如 .zip
、.tar
、.gz
等)解压缩时利用恶意构造的文件名,突破预期的文件目录限制,进而覆盖或创建系统中不应访问的文件。攻击者可能通过构造恶意的路径,实现远程代码执行(RCE)或覆盖系统中的关键文件。Python 在处理压缩文件时,如果不进行安全验证,也可能遭遇此类攻击。
当一个压缩包中包含恶意构造的文件路径(例如 ../../etc/passwd
),解压程序如果没有适当的检查,可能会将这些路径视为相对路径,从而将文件解压到系统的敏感目录。这种攻击称为路径遍历(Directory Traversal)。
- demo1
import zipfile
import os
# ../../etc/passwd attack
def extract_zip(zip_file, extract_to):
with zipfile.ZipFile(zip_file, 'r') as zf:
zf.extractall(extract_to) # 直接解压所有文件
- demo2
# 即使做了 后缀检验 防不住 ../../../var/www/html
def extract_html(request):
filename = request.FILES["filename"]
zf = zipfile.ZipFile(filename.temporary_file_path(), "r")
for entry in zf.namelist():
if entry.endswith(".html"):
file_content = zf.read(entry)
with open(entry, "wb") as fp:
fp.write(file_content)
zf.close()
return HttpResponse("HTML files extracted!")
正则绕过
-
re.search
:扫描整个字符串,直到找到第一个符合正则表达式的匹配项 -
re.match
-
re.match
仅从字符串开头开始匹配,不会匹配换行符 - 使用
re.MULTILINE
标志可以让^
和$
匹配每行的开头和结尾 - 使用
re.DOTALL
标志可以让.
匹配包括新行在内的所有字符
-
import re
def is_sql_injection(test_str):
pattern = re.compile(r".*(union)|(select).*")
if re.search(pattern, test_str):
return True
return False
def is_sql_injection_match(test_str):
pattern = re.compile(r".*(union)|(select).*")
if re.match(pattern, test_str):
return True
return False
def is_sql_injection_match_fix(test_str):
pattern = r".*(union)|(select).*"
if re.match(pattern, test_str, re.DOTALL):
return True
return False
test_a = "union xxxasd"
test_b = "xxxasd \n\
union ASDSDFF"
print(is_sql_injection(test_a))
print(is_sql_injection(test_b))
print(is_sql_injection_match(test_a))
print(is_sql_injection_match(test_b))
print(is_sql_injection_match_fix(test_a))
print(is_sql_injection_match_fix(test_b))
>>>
[root@localhost python_sec]# python3 demo5.py
True
True
True
False
True
True
备注:re.DOTALL后不可使用预编译compile,会导致性能有损
编码的问题
不同编码规则的差异会导致一些绕过问题,比如漏洞防御代码绕过等。这里拿unicode进行举例
- unicode编码绕过过滤
import unicodedata
from django.shortcuts import render
from django.utils.html import escape
def render_input(request):
user_input = escape(request.GET["p"])
normalized_user_input = unicodedata.normalize("NFKC", user_input)
context = {"my_input": normalized_user_input}
return render(request, "test.html", context)
demo十分简单,先进行escape后unicodedata.normalize解码,那么
%EF%B9%A4 -> <
%EF%B9%A5 -> <
利用unicode编码成功绕过xss防御,将恶意payload注入到render函数执行
- unicode大小写问题
from django.core.mail import send_mail
from django.http import HttpResponse
from vuln.models import User
def reset_pw(request):
email = request.GET["email"]
result = User.objects.filter(email__exact=email.upper()).first()
if not result:
return HttpResponse("User not found!")
send_mail(
"Reset Password",
"Your new pw: 123456.",
"from@example.com",
[email],
fail_silently=False,
)
return HttpResponse("Password reset email sent!")
demo问题点出在 先super操作,很多字符大写后会被兼容转换
foo@mıx.com -> FOO@MIX.COM
foo@mix.com -> FOO@MIX.COM
攻击者可以利用上述绕过手法进行绕过
反序列化
python的反序列化分为 pickle 和 pyyaml两种,这里不做赘述
pip相关
Python 的 pip
是 Python 包管理工具,负责安装和管理 Python 包。随着供应链攻击成为网络安全的一个重要问题,确保 pip
生态系统的安全性变得尤为重要
- 使用可信的源:指定官方源:优先使用官方的 Python 包索引(PyPI)源 (
pypi.org/simple
)。避免使用不可信的第三方源。
pip install --index-url=https://pypi.org/simple <package>
- 验证包的完整性
pip install --require-hashes -r requirements.txt
- requirement指定hash值
package==1.0.0 --hash=sha256:hashvalue
-
更新操作:这部分建议非必要不更新,防止黑客投毒
-
...etc等
写在最后
持续整理一些python的相关安全问题
参考
https://deepsource.com/blog/python-security-pitfalls
https://www.sonarsource.com/blog/10-unknown-security-pitfalls-for-python/