Python Web安全实战:SQL注入防御与JWT认证最佳实践
1. 项目概述:为什么安全编程是Python进阶的必修课?
在Python的世界里,从写几行脚本处理数据,到构建一个对外服务的Web应用,中间隔着一道巨大的鸿沟,这道鸿沟的名字就叫“安全”。很多开发者,尤其是从数据分析、自动化脚本切入的朋友,往往在功能实现上突飞猛进,却在安全防线上“裸奔”。我见过太多因为一个简单的SQL拼接,导致整个数据库被拖走的案例;也调试过不少因为认证逻辑有瑕疵,让普通用户能越权访问管理员页面的系统。所以,当你的Python技能树点向Web开发、API服务时,“安全编程”就不再是一个可选项,而是你必须装备上的铠甲。
今天,我们就聚焦两个最具代表性也最危险的安全议题:SQL注入和JWT认证。选择它们,是因为它们一攻一防,恰好构成了Web应用安全的一个经典剖面。SQL注入是“矛”,是攻击者最常用、最直接的武器之一,利用的是开发者对用户输入的天真信任。而JWT(JSON Web Token)是现代无状态认证的“盾”,但盾如果铸造得不好,反而会成为攻击者潜入内部的“后门”。通过这个实战,我希望你能掌握的不仅仅是如何写几行防御代码,更是建立起一套“不信任任何外部输入”的安全编程思维。无论你是在用Django、Flask还是FastAPI,这套思维都是通用的。
2. 核心威胁解析:SQL注入的原理与危害
2.1 SQL注入是如何发生的?一个经典的漏洞场景
要理解如何防御,必须先透彻理解攻击是如何发生的。我们来看一个最经典的错误示例,假设我们有一个用户登录的功能:
# 危险!典型的SQL注入漏洞代码 username = request.form['username'] password = request.form['password'] sql = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'" cursor.execute(sql)这段代码看起来人畜无害,但它将用户输入直接拼接到了SQL语句中。如果用户在用户名输入框里输入的不是“admin”,而是admin' --,会发生什么?拼接后的SQL语句变成了:
SELECT * FROM users WHERE username = 'admin' -- ' AND password = 'anything'在SQL中,--是注释符,这意味着后面的AND password = 'anything'整个被注释掉了。这条语句等价于SELECT * FROM users WHERE username = 'admin'。攻击者无需知道密码,就能以管理员身份登录。这还只是最简单的“绕过登录”。更危险的payload可能是' OR '1'='1,甚至是联合查询语句,用于窃取数据库中的所有数据。
其根本原因在于,程序代码(SQL指令)和用户提供的数据(用户名、密码)没有做到清晰的分离。数据库引擎无法区分哪部分是程序员意图执行的指令,哪部分是用户提供的数据,它忠实地执行了拼接后的整条字符串,导致用户输入被“提升”成了程序逻辑的一部分。
2.2 攻击者的工具箱:不止于绕过登录
很多初学者以为SQL注入就是用来“盗号”的,其实它的危害远不止于此。一个成功的SQL注入点,在攻击者手中就像一把能打开数据库大门的万能钥匙。
- 数据泄露:这是最主要的目的。通过
UNION SELECT语句,攻击者可以读取数据库中的任何表,比如用户表、订单表,甚至是存储敏感信息的配置表。 - 数据篡改:利用
UPDATE或DELETE语句,攻击者可以修改商品价格、清空用户账户余额,或者删除关键业务数据。 - 权限提升:在某些数据库(如PostgreSQL)中,如果应用使用的数据库连接权限过高,攻击者可能执行
CREATE USER或GRANT语句,为自己创建后门账户。 - 读取服务器文件:利用像
LOAD_FILE()(MySQL)这样的函数,攻击者可能读取服务器上的敏感文件,如配置文件、SSH密钥。 - 甚至执行系统命令:在极端情况下(如SQL Server的
xp_cmdshell),SQL注入可能导致攻击者在数据库服务器上执行任意系统命令,完全控制服务器。
理解这些危害,你就能明白为什么SQL注入长期位居OWASP Top 10(开放式Web应用程序安全项目十大安全风险)的前列。它不是一种“理论性”风险,而是每天都在真实发生的攻击。
3. 防御实战:从根源上杜绝SQL注入
知道了原理,防御就有了明确的方向:核心就是让“代码”和“数据”分家,确保数据库永远将用户输入视为纯粹的“数据”,而不是可执行的“代码”。以下是几种经过实战检验的、层层递进的防御方案。
3.1 第一道防线:参数化查询(预编译语句)
这是防御SQL注入的黄金标准,也是你首先应该采用的方法。几乎所有现代数据库驱动和ORM都支持它。
原理:参数化查询将SQL语句的“骨架”和“数据”分开处理。你首先定义一个带占位符(如%s,?,:name)的SQL模板,然后将用户输入的数据作为参数单独传递给数据库驱动。数据库会先编译SQL模板(确定执行计划),然后再将参数值代入。因为参数值是在编译后才传入的,所以无论它里面包含什么SQL关键字或特殊字符,都只会被当作普通字符串数据来处理。
以Python的sqlite3和psycopg2(PostgreSQL)为例:
# 使用 sqlite3(使用 ? 作为占位符) import sqlite3 conn = sqlite3.connect('app.db') cursor = conn.cursor() username = request.form['username'] password = request.form['password'] # 正确做法:参数化查询 sql = "SELECT * FROM users WHERE username = ? AND password = ?" cursor.execute(sql, (username, password)) # 数据以元组形式传入 # 使用 psycopg2(使用 %s 作为占位符) import psycopg2 conn = psycopg2.connect("dbname=test user=postgres") cursor = conn.cursor() sql = "SELECT * FROM users WHERE username = %s AND password = %s" cursor.execute(sql, (username, password)) # 注意:这里仍然是 %s,但数据单独传参关键心法:这里
?和%s是占位符,不是字符串格式化操作。千万不要写成cursor.execute(sql % (username, password)),那又变回危险的字符串拼接了。
3.2 第二道防线:使用ORM(对象关系映射)
如果你在使用Django、SQLAlchemy、Peewee等ORM框架,那么恭喜你,你已经天然地获得了一层强大的保护。ORM的核心思想是将数据库表映射为编程语言中的类,将行映射为对象。所有的数据库操作都通过操作对象和方法来完成,ORM在底层会自动为你生成参数化查询。
以SQLAlchemy Core为例:
from sqlalchemy import create_engine, text engine = create_engine('sqlite:///app.db') username = request.form['username'] # 安全:使用 text() 构造和参数绑定 sql = text("SELECT * FROM users WHERE username = :username") result = engine.execute(sql, username=username) # 参数通过命名方式绑定 # 或者更常见的,使用SQLAlchemy ORM的查询API from sqlalchemy.orm import Session from mymodels import User session = Session(engine) # 这种方式完全避免了手写SQL,是最安全的 user = session.query(User).filter(User.username == username).first()ORM的优势:除了安全,它还提高了开发效率、代码可读性和可维护性。你几乎不需要手写原始的SQL语句。
实操心得:即使在使用ORM时,也务必警惕一种罕见但危险的操作:
session.execute(f"SELECT * FROM {table_name}")。如果table_name来自用户输入,这仍然可能导致注入(称为“二阶SQL注入”或“ORM注入”)。永远不要让用户输入直接参与SQL字符串的任何部分拼接,包括表名、列名。
3.3 辅助措施:输入验证与转义
参数化查询是治本之策,但输入验证和转义可以作为有益的补充。
输入验证:在数据到达数据库层之前就进行过滤。例如,如果用户名只允许字母数字,那么就用正则表达式拒绝任何包含单引号、分号等特殊字符的输入。这能挡住大部分“瞎试”的自动化攻击脚本。
import re if not re.match(r'^[a-zA-Z0-9_]+$', username): return "用户名包含非法字符"但记住,验证不能替代参数化查询。因为业务逻辑可能允许用户名包含引号(如
O‘Connor),这时验证就会误杀合法用户,而参数化查询则能正确处理。转义:对用户输入中的特殊字符进行转义,使其失去特殊含义。例如,将单引号
‘转义为\‘。Python的标准库psycopg2.extensions、MySQLdb等都有escape_string方法。from psycopg2.extensions import adapt escaped_username = adapt(username).getquoted()但是!我强烈建议你不要依赖转义作为主要防御手段。原因有三:第一,转义规则因数据库而异(MySQL、PostgreSQL、SQLite的规则不同),容易出错;第二,如果忘记对某个字段转义,防线就崩溃了;第三,在复杂的查询中,很难保证所有地方都正确转义。把它看作最后一道应急的、数据库特定的补救措施,而不是首选方案。
4. 认证演进:从Session到无状态JWT
解决了数据层的注入风险,我们再来看看访问控制的核心——认证。传统的基于Session的认证在单体应用中工作良好,但在微服务、前后端分离的架构下显得笨重。每个服务都需要能访问中央Session存储(如Redis)来验证用户状态,这增加了复杂性和网络开销。
于是,JWT(JSON Web Token)应运而生。它是一种开放标准(RFC 7519),用于在各方之间作为JSON对象安全地传输信息。其核心思想是:服务器在验证用户身份后,生成一个包含用户信息的令牌(Token),并对其进行签名。客户端在后续请求中携带此令牌,服务器只需验证签名即可确认令牌的有效性和其中的用户信息,无需查询数据库或Session存储。
4.1 JWT的组成:三部分拆解
一个JWT看起来像这样:xxxxx.yyyyy.zzzzz,由三部分组成,用点分隔。
Header(头部):一个JSON对象,通常包含令牌类型(
typ: “JWT”)和所使用的签名算法(alg: “HS256”),然后经过Base64Url编码。{ "alg": "HS256", "typ": "JWT" }Payload(负载):也是一个JSON对象,包含了要传递的“声明”(Claims)。声明分为三种:
- 注册声明:预定义的一些标准字段,如
iss(签发者)、exp(过期时间)、sub(主题)等。 - 公共声明:可以自定义的字段,但为了避免冲突,应使用IANA JWT注册表定义的名字或包含一个抗冲突命名空间(如一个URI)。
- 私有声明:自定义的字段,用于在同意使用它们的各方之间共享信息。这是我们最常用的部分,比如存放
user_id、username、role。
{ "sub": "1234567890", "name": "John Doe", "user_id": 42, "admin": true, "iat": 1516239022, // issued at,签发时间 "exp": 1516242622 // expiration time,过期时间 }重要提示:Payload只是经过Base64Url编码,并没有加密。任何人都可以解码看到里面的内容。因此,绝对不要在JWT的Payload中存放任何敏感信息,如密码、信用卡号。
- 注册声明:预定义的一些标准字段,如
Signature(签名):这是JWT安全性的关键。签名通过对编码后的Header、编码后的Payload、一个密钥(Secret)使用Header中指定的算法(如HS256)计算得出。
HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret )签名用于验证消息在传输过程中没有被篡改。只要密钥不泄露,任何人都无法伪造一个有效的签名。
4.2 JWT的工作流程
理解了结构,流程就清晰了:
- 登录:用户提交凭证(如用户名密码)。服务器验证通过后,生成JWT(包含用户ID、角色等信息,并设置过期时间),将其返回给客户端(通常放在HTTP响应体或Cookie中)。
- 携带令牌:客户端在后续请求中携带JWT,通常放在HTTP请求头的
Authorization字段中,格式为Bearer <token>。 - 验证令牌:服务器收到请求,从
Authorization头中取出JWT,使用相同的密钥验证其签名。如果签名有效且令牌未过期,则信任Payload中的用户信息,并据此进行授权。
这个流程实现了无状态:服务器不需要保存会话信息,所有必要信息都包含在令牌本身中。这使得水平扩展服务变得非常容易。
5. JWT实战:在Python中安全地实现与使用
理论很美好,但实战中陷阱很多。下面我们用PyJWT这个最流行的库来演示如何正确、安全地使用JWT。
5.1 安装与基础使用
首先安装库:pip install PyJWT
import jwt import datetime from functools import wraps from flask import request, jsonify, Flask app = Flask(__name__) # 这是一个非常重要的密钥,必须足够复杂且妥善保管! SECRET_KEY = "your-very-secret-and-long-key-change-this-in-production" def generate_jwt(user_id, username, role='user'): """ 生成JWT令牌 """ # 设置过期时间,例如30分钟后过期 expiration = datetime.datetime.utcnow() + datetime.timedelta(minutes=30) # 构建Payload payload = { 'user_id': user_id, 'username': username, 'role': role, 'exp': expiration, # 标准声明:过期时间 'iat': datetime.datetime.utcnow(), # 标准声明:签发时间 'iss': 'your-app-name' # 标准声明:签发者 } # 使用HS256算法和密钥生成令牌 token = jwt.encode(payload, SECRET_KEY, algorithm='HS256') # 注意:在PyJWT>=2.0.0版本,encode返回的是字符串。之前版本返回字节,可能需要.decode() return token @app.route('/login', methods=['POST']) def login(): # 1. 验证用户凭证(这里简化,实际应从数据库验证) username = request.json.get('username') password = request.json.get('password') # ... 验证逻辑 ... user = authenticate(username, password) # 假设的验证函数 if user: # 2. 生成JWT token = generate_jwt(user.id, user.username, user.role) return jsonify({'token': token}) else: return jsonify({'error': 'Invalid credentials'}), 4015.2 编写JWT认证装饰器
为了保护需要认证的接口,我们可以编写一个装饰器来自动验证请求中的JWT。
def token_required(f): """ 认证装饰器:验证请求头中的JWT令牌 """ @wraps(f) def decorated_function(*args, **kwargs): token = None # 从请求头中获取令牌,格式应为 "Bearer <token>" auth_header = request.headers.get('Authorization') if auth_header and auth_header.startswith('Bearer '): token = auth_header.split(' ')[1] if not token: return jsonify({'message': 'Token is missing!'}), 401 try: # 验证并解码令牌 # `options`参数可以设置验证选项,这里我们要求验证签名和过期时间 data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'], options={'verify_exp': True}) # 将解码后的用户信息存入请求上下文,方便视图函数使用 request.current_user = data except jwt.ExpiredSignatureError: return jsonify({'message': 'Token has expired!'}), 401 except jwt.InvalidTokenError as e: # 捕获其他所有无效令牌错误(如签名错误、格式错误等) return jsonify({'message': 'Token is invalid!', 'error': str(e)}), 401 return f(*args, **kwargs) return decorated_function # 在需要保护的接口上使用装饰器 @app.route('/protected') @token_required def protected_route(): # 可以从 request.current_user 中获取用户信息 current_user = request.current_user return jsonify({ 'message': f'Hello, {current_user["username"]}!', 'user_id': current_user['user_id'] })5.3 关键安全配置与最佳实践
仅仅能生成和验证JWT是远远不够的,以下这些实践决定了你的认证系统是铜墙铁壁还是纸糊的窗户。
使用强算法并验证算法(Critical):
- 绝对禁止使用
‘none‘算法:有些库支持‘none‘算法,表示不签名。攻击者可以篡改Payload后,将Header中的算法改为‘none‘,如果服务器不验证算法类型,就会接受这个被篡改的令牌。在jwt.decode()时,务必通过algorithms参数明确指定你接受的算法列表,如algorithms=[‘HS256‘]。
# 安全做法:明确指定算法 decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) # 危险做法:不指定算法,可能接受‘none‘ # decoded = jwt.decode(token, SECRET_KEY) # 不要这样写!- 绝对禁止使用
设置合理的过期时间(
exp):JWT一旦签发,在过期前无法被服务器主动废止。因此,过期时间不宜过长。对于Web应用,通常设置为15分钟到几小时。通过刷新令牌(Refresh Token)机制来获取新的访问令牌(Access Token),可以兼顾安全性和用户体验。妥善保管密钥(
SECRET_KEY):HS256算法的安全性完全依赖于密钥。这个密钥必须:- 足够长且随机(建议32字节以上)。
- 存储在环境变量或安全的配置管理服务中,绝不能硬编码在代码里或提交到版本库。
- 生产环境与开发/测试环境使用不同的密钥。
Payload中不存放敏感数据:再次强调,Payload是Base64编码,等同于明文传输。只存放用于标识和授权的最小必要信息,如
user_id,role。考虑使用非对称加密算法(RS256):对于大型分布式系统,考虑使用RS256(RSA签名)。服务器用私钥签名,所有服务都用公钥验证。这样,私钥可以安全地保存在认证服务器上,即使公钥泄露也无法伪造令牌。
6. 常见安全陷阱与进阶防护
即使遵循了上述实践,JWT仍然有一些固有的安全考量需要你特别注意。
6.1 JWT的“无法废止”问题与解决方案
这是JWT被诟病最多的一点。由于服务器无状态,它无法像使Session失效一样,让一个尚未过期的JWT立即失效。如果用户的令牌被盗,或者需要强制下线某个用户,在令牌自然过期前,系统是无能为力的。
解决方案:
- 短期令牌+刷新令牌:将访问令牌(Access Token)的过期时间设得很短(如15分钟),同时颁发一个有效期较长的刷新令牌(Refresh Token)。刷新令牌单独存储在服务器的数据库或缓存中(因此是有状态的)。当访问令牌过期后,客户端用刷新令牌去换取新的访问令牌。如果需要废止用户,只需在服务器端将该用户的刷新令牌删除或加入黑名单即可。
- 令牌黑名单:维护一个已注销但未过期的令牌黑名单(存储在Redis或数据库)。每次验证令牌时,除了检查签名和过期时间,还要查询该令牌ID(可以在Payload中加入一个唯一的
jti字段)是否在黑名单中。这适用于需要立即废止令牌的场景,但引入了状态,部分牺牲了无状态的优势。 - 动态密钥/密钥轮转:定期更换签名密钥。旧密钥签发的令牌在密钥轮转后就会失效。这需要协调所有服务,并处理好新旧令牌的过渡期。
6.2 防范重放攻击
重放攻击是指攻击者截获一个有效的请求(包含有效的JWT),然后原封不动地重复发送给服务器。例如,重复提交一个转账请求。
解决方案:
- 使用一次性令牌(Nonce):对于关键操作(如支付、修改密码),可以在Payload中加入一个一次性随机数(Nonce)和该操作唯一标识,服务器在处理请求时检查这个Nonce是否已被使用过,使用过则拒绝。这同样需要服务器端记录状态。
- 加入时间戳(
iat)并限制请求时间窗口:验证令牌的签发时间(iat),并拒绝签发时间过久的请求。例如,只接受过去5分钟内签发的令牌发起的支付请求。这能有效限制重放攻击的时间窗口。
6.3 存储与传输安全
- 客户端存储:不要将JWT存储在
localStorage或sessionStorage中,因为它们容易受到XSS(跨站脚本)攻击。更安全的做法是存储在HttpOnly的Cookie中,这样可以防止JavaScript访问,避免XSS攻击直接窃取令牌。但要注意防范CSRF(跨站请求伪造)攻击,可以为Cookie设置SameSite=Strict或Lax属性,并使用CSRF Token进行额外保护。 - 服务端验证:永远在服务器端验证JWT签名。绝对不要相信客户端解码后传回的结果。
7. 实战整合:构建一个带基础防护的Flask API
让我们把SQL注入防御和JWT认证整合到一个简单的Flask应用中,形成一个最小化的安全实践样板。
from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy import jwt import datetime from functools import wraps from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) app.config['SECRET_KEY'] = 'your-secret-key-change-this' # 应从环境变量读取 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db' app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db = SQLAlchemy(app) # 1. 定义模型 - 使用ORM是防御SQL注入的第一层 class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(200), nullable=False) # 存储哈希,而非明文密码 def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) # 2. JWT工具函数 def generate_token(user_id): try: payload = { 'exp': datetime.datetime.utcnow() + datetime.timedelta(minutes=30), 'iat': datetime.datetime.utcnow(), 'sub': user_id } return jwt.encode(payload, app.config['SECRET_KEY'], algorithm='HS256') except Exception as e: return str(e) def token_required(f): @wraps(f) def decorated(*args, **kwargs): token = request.headers.get('Authorization') if not token or not token.startswith('Bearer '): return jsonify({'message': 'Token is missing or invalid!'}), 401 token = token.split(' ')[1] try: # 明确指定算法,防止算法混淆攻击 data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256']) current_user_id = data['sub'] # 将当前用户ID存入g对象或request上下文 request.current_user_id = current_user_id except jwt.ExpiredSignatureError: return jsonify({'message': 'Token has expired!'}), 401 except jwt.InvalidTokenError: return jsonify({'message': 'Token is invalid!'}), 401 return f(*args, **kwargs) return decorated # 3. 安全的路由 @app.route('/register', methods=['POST']) def register(): """用户注册 - 使用ORM添加用户,避免SQL注入""" data = request.get_json() username = data.get('username') password = data.get('password') if not username or not password: return jsonify({'message': 'Username and password required'}), 400 if User.query.filter_by(username=username).first(): # ORM查询,安全 return jsonify({'message': 'Username already exists'}), 400 new_user = User(username=username) new_user.set_password(password) # 密码哈希存储 db.session.add(new_user) db.session.commit() return jsonify({'message': 'User registered successfully'}), 201 @app.route('/login', methods=['POST']) def login(): """用户登录 - 使用ORM查询和密码哈希验证""" data = request.get_json() username = data.get('username') password = data.get('password') # 使用ORM的filter_by进行查询,从根本上杜绝SQL注入 user = User.query.filter_by(username=username).first() if user and user.check_password(password): token = generate_token(user.id) return jsonify({'token': token}) else: return jsonify({'message': 'Invalid credentials!'}), 401 @app.route('/profile') @token_required def get_profile(): """获取用户资料 - 需要JWT认证""" user_id = request.current_user_id # 再次使用ORM,通过主键ID安全查询 user = User.query.get(user_id) if not user: return jsonify({'message': 'User not found'}), 404 return jsonify({'id': user.id, 'username': user.username}) @app.route('/search') def search_users(): """一个模拟的搜索接口 - 展示即使有查询参数,也应使用ORM安全处理""" keyword = request.args.get('q', '') # 危险做法(永远不要用): # users = db.session.execute(f"SELECT * FROM user WHERE username LIKE '%{keyword}%'") # 安全做法:使用ORM的filter和参数绑定 # SQLAlchemy会自动处理参数化,即使使用LIKE和通配符 users = User.query.filter(User.username.like(f'%{keyword}%')).all() # 注意:这里将用户输入的keyword直接放入like模式字符串中。 # 在极端情况下,如果keyword本身包含%或_通配符,可能会影响查询预期。 # 更严谨的做法是对keyword中的通配符进行转义,但这与SQL注入防御是不同层面的问题。 result = [{'id': u.id, 'username': u.username} for u in users] return jsonify(result) if __name__ == '__main__': with app.app_context(): db.create_all() # 创建数据表 app.run(debug=True) # 生产环境务必关闭debug模式!这个示例整合了关键的安全实践:
- 全程使用SQLAlchemy ORM,从根本上避免了手写SQL拼接。
- 密码加盐哈希存储(使用Werkzeug),即使数据库泄露,攻击者也无法直接获得明文密码。
- JWT认证流程完整,包括生成、验证和装饰器保护。
- 密钥从配置读取(示例中硬编码了,实际应使用环境变量)。
- 验证JWT时明确指定算法。
8. 总结与持续学习
安全是一个持续的过程,而不是一个可以一劳永逸的特性。通过本次实战,我希望你牢牢记住两个核心心法:对于SQL,永远让数据和指令分离(参数化查询/ORM);对于认证,永远不要信任客户端传来的任何身份声明,必须用密码学手段验证(JWT签名)。
在实际开发中,除了注入和认证,你还需要关注其他OWASP Top 10中的威胁,如跨站脚本(XSS)、跨站请求伪造(CSRF)、不安全的反序列化等。建议你将安全扫描工具(如Bandit for Python, OWASP ZAP)集成到CI/CD流程中,定期进行依赖项漏洞检查(如pip-audit,safety),并保持对所用框架安全公告的关注。
最后,安全意识和习惯比任何单一技术都重要。在写下每一行处理外部输入的代码时,都下意识地问自己:“如果用户在这里输入最恶意的内容,我的程序会怎样?” 带着这种“怀疑一切”的思维去编程,你构建的系统才会真正坚固。
