跳转至

12.0 Security

Security Basics

开发 Web 应用程序时,安全性是一个首要关注点。核心假设是网络本身是不可信的。必须控制数据访问,安全地验证密码,并且用户必须能够信任呈现给他们的信息。完全的安全性非常难以实现,超出了本课程的范围。

Web 安全依赖于信任。主要包含几个要素: 1. Web 服务器需要确信访问数据的人已授权。这通过会话/基于令牌的身份验证来实现。 2. 用户需要知道他们访问的网站是他们想要的网站。这通过 SSL Certificates 来实现。 3. 服务器和客户端都需要确信中间没有人未经授权访问数据。这通过 HTTPS (HTTP Secure) 来实现。

这些技术的基础是Public-Key Encryption(公钥加密)和 Cryptographic Hashing(密码散列)。

Public Key Encryption

Secure communication 基于 Public Key Encryption。它涉及一对密钥:pub(公钥)和 priv(私钥)。公钥可以发布,但即使知道公钥也无法计算出私钥。加密函数 Ek(•) 使用密钥 k 进行加密,解密函数 Dk(•) 使用密钥 k 进行解密。对于一对公私钥,满足 x = Dpriv(Epub(x))x = Dpub(Epriv(x))

Public Key Encryption 的用途包括: * Authentication(身份验证):Epub(x) 的值可以发布,只有知道 priv 的人通过计算 Dpriv(Epub(x)) 才能得出 x。 * Digital Signatures(数字签名):一对 (x, Epriv(x)) 可以通过计算 Dpub(Epriv(x)) 并与 x 比较来验证,但只有知道 priv 的人才能创建。 * Key Distribution(密钥分发):可以生成一个新的随机密钥 x,然后 Epub(x) 可以发送给知道 priv 的人。这样只有这对双方知道 x。

Cryptographic Hashing

Secure data storage 基于 Cryptographic Hashing。密码散列函数 hash 接受任意长度的字符串,返回一个固定长度的整数。其关键属性包括: * 给定一个值 h,很难找到一个 x 使得 hash(x) = h。 * 给定一个值 x,很难找到一个 y 使得 hash(x) = hash(y)

本质上,它从数据流计算出一个数字,使得伪造具有特定散列值的数据非常困难。这与用于散列表的散列不同,密码散列必须满足更强的属性。

它可用于: * Storing Passwords Securely(安全存储密码):给定密码 p,可以在数据库中存储 h = hash(p) 而不是 p。当用户使用密码 q 尝试登录时,可以测试 hash(q) = h 是否成立。

Best practices for hashing:密码散列函数的设计非常困难。切勿尝试创建自己的散列函数或混用不同的散列函数。

Session-Based User Authentication

虽然 HTTP 是无状态的,但应用程序不是。为了跟踪用户的 session,需要用户注册,以便将用户名和密码与他们关联。密码被加盐(salted)散列(hashed),结果存储在数据库中。用户登录时提供相同的密码,再次加盐和散列生成一个摘要,与数据库中的散列进行比较。如果成功,服务器响应会包含一个带有 session ID 和 hash(ID + secret) 的 cookie。随后的 HTTP 请求中,服务器会检查 cookie 中的 session ID 并重新计算。

Flask 通过一个 "secret key" 来实现 session 的概念。当 Flask 配置了 secret key,它会自动包含一个签名的 cookie。将 secret key 存储在源文件中是个坏主意,因为它会被推送到 Git 仓库。更好的做法是通过系统环境变量设置 secret key,并在应用程序运行时加载。

Flask-Login 包提供了一个使用现有 session 基础设施实现登录的接口。安装后,需要在 __init__.py 中创建 LoginManager 实例。需要设置 login_view 指向登录页面的路由名称。Flask-Login 需要知道如何加载“user”对象。 @login.user_loader 装饰器用于标记将 ID 映射到用户的函数,通常在 models.py 中。

User model 需要实现几个方法和属性供 Flask-Login 使用: * is_authenticated:用户是否拥有有效的登录凭据。 * is_active:用户是否被允许登录。 * is_anonymous:用户是否为匿名用户。 * get_id():返回用户的唯一 ID。

可以使用 UserMixin 来实现这些方法,它提供了合理的默认值。

Flask-Login 提供了实用的工具: * current_user:当前登录的用户模型。如果未登录,则返回特殊的匿名用户对象。可以在视图函数中使用 current_user.is_authenticated 来检查用户是否登录。 * login_user():将 current_user 设置为指定的用户模型。 * logout_user():将 current_user 设置回匿名用户。

@login_required 装饰器用于标记需要登录的路由。未登录用户尝试访问此路由时,将自动收到未经授权的响应。current_user 变量也可以在 Jinja 模板中使用,控制页面元素的可见性或个性化页面。

Salting your hashes

仅对密码进行散列不足以保证安全。如果攻击者获取了数据库,他们可以通过彩虹表(rainbow tables)预先计算常用密码的散列值。解决方案是使用随机的 salt 加盐密码 p,并在数据库中存储 (s, hash(p + s)),而不是 hash(p)。由于每个用户的 salt 不同,攻击者无法预先计算彩虹表。 werkzeug 的 generate_password_hash 函数会自动处理加盐。

Example (Conceptual): 用户注册密码 "password123"。 生成一个随机 salt,例如 "xyz"。 存储 hash("password123" + "xyz") 和 "xyz" 在数据库。 用户登录输入 "password123"。 从数据库获取存储的 salt "xyz"。 计算 hash("password123" + "xyz")。 将计算出的散列与数据库中存储的散列进行比较。

Configuring secret keys

Secret keys 和第三方服务的凭据应该手动配置,并且永远不要存储在 Git 仓库的版本控制下。更好的方法是通过系统环境变量设置它们。可以创建一个配置文件来存储所有配置变量,并在应用程序运行时加载。

Example: 在 Linux/macOS 中设置环境变量: export SECRET_KEY='this-is-a-very-secret-key' 在 Flask 配置中读取环境变量: import osclass Config:SECRET_KEY = os.environ.get('SECRET_KEY') or 'default-fallback-key' (注意:实际应用中应确保环境变量已设置,避免使用默认回退密钥,或仅在开发环境使用)

Attacks and Defences

尽管有 session 机制,但网络仍然是根本不安全的。

Impersonation attack

Description(描述):网络将用户的请求重定向到伪造的服务器。伪造的服务器行为与真实服务器相同,用户向伪造服务器提交密码、私有数据等敏感信息。 Defence(防御):服务器通过 Secure Socket Layer (SSL) protocol 提供的证书来证明其真实性。证书由可信第三方 Certificate Authority (CA) 提供。服务器向 CA 发送其公钥 s_pub,CA 验证后返回 (s_pub, Ec_priv(s_pub)),其中 c_priv 是 CA 的私钥。用户联系服务器时,服务器发送 (s_pub, Ec_priv(s_pub))。用户使用 CA 的公钥 c_pub 来比较 s_pub 和 Dc_pub(Ec_priv(s_pub))。如果匹配,用户就知道 CA 担保服务器拥有公钥 s_pub,只有真实服务器知道私钥 s_priv。CA 的信任问题通过根证书(root certificates)解决。

Man-in-the-middle attack (MiTM)

Description(描述):即使使用证书,网络仍可能执行 MiTM 攻击。所有流量首先被重定向到攻击者,然后转发给真实服务器。用户与真实服务器交互,证书检查通过。但攻击者可以访问所有来往信息,包括签名的 cookie. Defence(防御):无法阻止网络重定向消息,因此必须加密消息。SSL 包含一个 public key encryption 过程来实现安全 HTTP 请求。在证书握手后,双方使用密钥分发协议生成一组共享的私有 session 密钥。然后使用这些密钥加密会话期间的所有后续流量。HTTP over SSL 称为 HTTPS (HTTP Secure)。现代浏览器会标记不使用 HTTPS 的网站。

Cross-site request forgery (CRSF) attack

Description(描述):攻击者利用用户可能已经在服务器上进行身份验证的事实。攻击者诱骗用户(例如通过电子邮件中的链接)向他们已登录的网站发出非预期的请求(例如,向银行网站发起转账)。浏览器会自动发送与该域名关联的 cookie。服务器检查 cookie,认为请求来自合法用户,并执行操作。 Defence(防御):服务器在渲染表单时,生成一个秘密令牌并将其包含在表单中。令牌是使用 Flask secret key 生成的随机数,攻击者无法伪造。提交表单时,秘密令牌validate_on_submit 期间被检查。这样,服务器只响应通过官方表单生成的请求。WTForms 提供了 CRSF tokens 来防御。此外,安全敏感的应用程序通常会非常快速地超时签名的 cookie(例如,约 5 分钟)。

Example (WTForms Token): 在 Flask 模板中渲染表单时,包含隐藏字段 {{ form.csrf_token }}。WTForms 会自动生成并验证此令牌。

Cross-site scripting (XSS) attack

Description(描述):绝不应直接包含未经验证的用户输入到可执行代码中。如果这样做,恶意用户可以通过使用非预期的输入逃逸(escape)当前语句并执行任意代码SQL Injection 是一种常见的 XSS 攻击。例如,直接构建 SQL 查询时,用户可以提供形如 "name; X" 的 ID,其中 "X" 是任意 SQL 语句,导致执行非预期或恶意的数据库操作。 Defence(防御):清理用户输入。最简单的方法是自动转义(escape)输入字符串中的任何控制字符。在 SQL 中涉及在字符前插入反斜杠。SQLAlchemy 的 query API 会自动转义任何传入的字符串,例如 Student.query.get(id) 会自动转义 id 中的所有内容。同样,Jinja 会自动转义任何替换到 HTML 模板中的字符串。其他方法包括输入验证和 prepared queries。

Example (SQL Injection): 假设有易受攻击的 SQL 查询: SELECT * FROM students WHERE id = '{user_input}' 恶意用户输入: ' OR '1'='1 实际执行的查询变成: SELECT * FROM students WHERE id = '' OR '1'='1',可能返回所有学生的数据。

Example (SQLAlchemy Defence): 使用 SQLAlchemy: Student.query.get(user_input_id) 即使 user_input_id 包含恶意字符串,SQLAlchemy 会自动转义,从而防止 SQL 注入。

Group Project Security Expectations

在小组项目中,不期望实现完整的安全性。期望做到: * 使用 Flask-Login 实现用户身份验证。 * 使用 SQLAlchemy 避免 Cross-Site Scripting 攻击。 * 使用 WTForms tokens 避免 CRSF 攻击。 * 正确地散列(hash)并加盐(salt)密码

不期望做到: * 获取 SSL 证书或设置 HTTPS。 * 实现基于令牌的身份验证。

Complete security 是非常难以实现的,本单元不会深入探讨。网络始终是不可信的。