`Content-Security-Policy` (CSP) 中的样式策略:`style-src` 与 `nonce` 的哈希验证

Content-Security-Policy (CSP) 中的样式策略:style-srcnonce 的哈希验证

大家好,今天我们要深入探讨 Content-Security-Policy (CSP) 中关于样式(CSS)安全的核心策略:style-src 指令和如何使用 nonce 和哈希值进行更精细的控制与验证。CSP 是一个强大的安全工具,旨在减轻跨站脚本攻击 (XSS) 风险,而 style-src 是 CSP 中用于管理 CSS 资源加载的关键组成部分。

CSP 简介与 style-src 的作用

CSP 本质上是一个声明式的安全策略,服务器通过 HTTP 响应头 Content-Security-Policy 将策略发送给浏览器。浏览器接收到策略后,会遵循策略的指示,决定哪些资源可以加载,哪些资源应该被阻止。

style-src 指令规定了哪些来源的 CSS 资源可以被加载。这包括:

  • 'self': 允许加载来自同一来源(协议、域名和端口)的 CSS。
  • 'unsafe-inline': 允许加载 HTML 文档中内联的 <style> 标签和 style 属性中的 CSS。 强烈不推荐,因为它会打开 XSS 的大门。
  • 'unsafe-eval': 允许使用 JavaScript 动态生成 CSS,例如使用 eval()new Function()同样不推荐,因为它会显著降低安全性。
  • data:: 允许使用 data URI 形式的 CSS (例如 data:text/css;base64,...)。 谨慎使用,可能增加攻击面。
  • https://example.com: 允许从指定域名的安全连接 (HTTPS) 加载 CSS。
  • *`.example.com`:** 允许从指定域名的所有子域名加载 CSS。
  • 'none': 阻止所有 CSS 加载。

为什么需要更精细的控制?

简单地使用 'self' 或指定域名可能不足以应对复杂的 Web 应用场景。攻击者可能会利用漏洞,在你的域名下上传恶意 CSS 文件,或者通过 XSS 将恶意 CSS 注入到页面中。为了解决这些问题,CSP 提供了 nonce 和哈希验证机制,允许你只允许特定的、可信的内联样式,从而大大提高安全性。

使用 nonce(一次性密码)

nonce 是一个随机字符串,服务器在生成 HTML 页面时动态生成,并在 CSP 策略和 <style> 标签中同时使用。浏览器会验证 <style> 标签的 nonce 属性是否与 CSP 策略中的 nonce 值匹配,只有匹配的内联样式才会被执行。

实现步骤:

  1. 服务器端生成 nonce 使用安全的随机数生成器生成一个强随机字符串。

    import secrets
    import base64
    
    def generate_nonce():
        return base64.b64encode(secrets.token_bytes(16)).decode('utf-8')
    
    nonce = generate_nonce()
    print(nonce)
  2. 在 CSP 策略中使用 noncenonce 添加到 style-src 指令中。

    Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-{nonce}'

    {nonce} 替换为实际生成的 nonce 值。

  3. nonce 添加到 <style> 标签: 将相同的 nonce 值添加到允许执行的 <style> 标签中。

    <style nonce="{nonce}">
      body {
        background-color: #f0f0f0;
      }
    </style>

    同样,将 {nonce} 替换为实际生成的 nonce 值。

示例代码 (Python Flask):

from flask import Flask, render_template_string, make_response
import secrets
import base64

app = Flask(__name__)

def generate_nonce():
    return base64.b64encode(secrets.token_bytes(16)).decode('utf-8')

@app.route('/')
def index():
    nonce = generate_nonce()
    template = """
    <!DOCTYPE html>
    <html>
    <head>
      <title>CSP with Nonce</title>
    </head>
    <body>
      <h1>CSP with Nonce Example</h1>
      <style nonce="{nonce}">
        body {
          background-color: #f0f0f0;
        }
      </style>
      <p>This is a test page.</p>
    </body>
    </html>
    """
    rendered_template = template.format(nonce=nonce)
    response = make_response(rendered_template)
    response.headers['Content-Security-Policy'] = f"default-src 'self'; style-src 'self' 'nonce-{nonce}'"
    return response

if __name__ == '__main__':
    app.run(debug=True)

注意事项:

  • 每次请求都必须生成新的 nonce 值。重复使用 nonce 会降低安全性,因为攻击者可能可以猜测到 nonce 值。
  • nonce 值必须是加密安全的随机数。
  • 确保服务器端和客户端使用的 nonce 值完全一致。
  • 不要在客户端生成 nonce,因为这会使其容易被攻击者操纵。

使用哈希值

哈希值是一种更精确的验证方法,它允许你只允许特定内容的内联样式。浏览器会对 <style> 标签的内容进行哈希运算,并将结果与 CSP 策略中的哈希值进行比较。如果两者匹配,则允许执行该样式。

实现步骤:

  1. 计算 CSS 内容的哈希值: 使用 SHA256、SHA384 或 SHA512 算法计算 <style> 标签内容的哈希值。

    import hashlib
    import base64
    
    def generate_style_hash(style_content, algorithm='sha256'):
        hash_object = hashlib.new(algorithm, style_content.encode('utf-8'))
        hash_value = base64.b64encode(hash_object.digest()).decode('utf-8')
        return f"'{algorithm}-{hash_value}'"
    
    style_content = """
    body {
      background-color: #f0f0f0;
    }
    """
    
    sha256_hash = generate_style_hash(style_content)
    print(sha256_hash)
  2. 在 CSP 策略中使用哈希值: 将哈希值添加到 style-src 指令中。

    Content-Security-Policy: default-src 'self'; style-src 'self' {sha256_hash}

    {sha256_hash} 替换为实际生成的哈希值。

  3. 将 CSS 内容添加到 <style> 标签: 将 CSS 内容添加到 <style> 标签中,不要添加任何属性 (例如 nonce)

    <style>
      body {
        background-color: #f0f0f0;
      }
    </style>

示例代码 (Python Flask):

from flask import Flask, render_template_string, make_response
import hashlib
import base64

app = Flask(__name__)

def generate_style_hash(style_content, algorithm='sha256'):
    hash_object = hashlib.new(algorithm, style_content.encode('utf-8'))
    hash_value = base64.b64encode(hash_object.digest()).decode('utf-8')
    return f"'{algorithm}-{hash_value}'"

@app.route('/')
def index():
    style_content = """
    body {
      background-color: #f0f0f0;
    }
    """
    sha256_hash = generate_style_hash(style_content)

    template = """
    <!DOCTYPE html>
    <html>
    <head>
      <title>CSP with Hash</title>
    </head>
    <body>
      <h1>CSP with Hash Example</h1>
      <style>
        body {
          background-color: #f0f0f0;
        }
      </style>
      <p>This is a test page.</p>
    </body>
    </html>
    """
    rendered_template = template
    response = make_response(rendered_template)
    response.headers['Content-Security-Policy'] = f"default-src 'self'; style-src 'self' {sha256_hash}"
    return response

if __name__ == '__main__':
    app.run(debug=True)

注意事项:

  • 哈希值必须与 <style> 标签的精确内容匹配,包括空格、换行符等。任何细微的更改都会导致哈希值不匹配,从而阻止样式执行。
  • 建议使用 SHA256 或更强的哈希算法。
  • 哈希值只能用于内联样式,不能用于外部 CSS 文件。
  • 如果你的 CSS 内容经常变化,那么使用哈希值可能会比较麻烦,因为每次更改都需要重新计算哈希值并更新 CSP 策略。

nonce vs. 哈希值:选择哪一个?

特性 nonce 哈希值
适用场景 动态生成的内联样式 静态的、不经常变化的内联样式
复杂性 较低,需要服务器端生成随机数并注入 较高,需要计算哈希值并确保精确匹配
灵活性 较高,允许动态修改样式,只需更新 nonce 较低,任何修改都需要重新计算哈希值
安全性 只要 nonce 是强随机数,安全性较高 只要哈希算法足够强,安全性很高
维护成本 较低 较高,尤其是当 CSS 内容经常变化时
  • nonce 更适合动态生成的内联样式, 例如,你的 CSS 样式依赖于用户的配置或其他动态数据。
  • 哈希值更适合静态的、不经常变化的内联样式, 例如,你有一个包含少量全局样式的 <style> 标签,并且这些样式很少更改。

最佳实践:

  • 尽可能避免使用 'unsafe-inline''unsafe-eval' 这是降低 XSS 风险的最重要一步。
  • 使用 nonce 或哈希值来控制内联样式。 选择哪种方法取决于你的具体需求和场景。
  • 使用 CSP 报告功能 ( report-urireport-to ) 来监控策略违规。 这可以帮助你发现潜在的安全问题并及时修复。
  • 逐步部署 CSP。 从报告模式开始,逐步收紧策略,并监控策略违规情况,确保不会影响用户体验。
  • 结合其他安全措施,例如输入验证、输出编码和 HTTP 安全头。 CSP 只是安全防线的一部分,不能单独使用。
  • 定期审查和更新 CSP 策略。 随着 Web 应用的发展,你的安全需求也会发生变化。

示例:同时使用 nonce 和哈希值

在某些情况下,你可能需要同时使用 nonce 和哈希值。例如,你可能有一些静态的内联样式,可以使用哈希值进行验证,同时也有一些动态生成的内联样式,需要使用 nonce 进行验证。

<!DOCTYPE html>
<html>
<head>
  <title>CSP with Nonce and Hash</title>
</head>
<body>
  <h1>CSP with Nonce and Hash Example</h1>
  <style>
    /* 静态样式 */
    body {
      font-family: sans-serif;
    }
  </style>
  <style nonce="{nonce}">
    /* 动态样式 */
    body {
      background-color: {user_background_color};
    }
  </style>
  <p>This is a test page.</p>
</body>
</html>
from flask import Flask, render_template_string, make_response
import secrets
import base64
import hashlib

app = Flask(__name__)

def generate_nonce():
    return base64.b64encode(secrets.token_bytes(16)).decode('utf-8')

def generate_style_hash(style_content, algorithm='sha256'):
    hash_object = hashlib.new(algorithm, style_content.encode('utf-8'))
    hash_value = base64.b64encode(hash_object.digest()).decode('utf-8')
    return f"'{algorithm}-{hash_value}'"

@app.route('/')
def index():
    nonce = generate_nonce()
    user_background_color = "#abcdef" # 假设从用户配置中获取

    static_style_content = """
    /* 静态样式 */
    body {
      font-family: sans-serif;
    }
    """
    sha256_hash = generate_style_hash(static_style_content)

    template = """
    <!DOCTYPE html>
    <html>
    <head>
      <title>CSP with Nonce and Hash</title>
    </head>
    <body>
      <h1>CSP with Nonce and Hash Example</h1>
      <style>
        /* 静态样式 */
        body {
          font-family: sans-serif;
        }
      </style>
      <style nonce="{nonce}">
        /* 动态样式 */
        body {{
          background-color: {user_background_color};
        }}
      </style>
      <p>This is a test page.</p>
    </body>
    </html>
    """
    rendered_template = template.format(nonce=nonce, user_background_color=user_background_color)
    response = make_response(rendered_template)
    response.headers['Content-Security-Policy'] = f"default-src 'self'; style-src 'self' {sha256_hash} 'nonce-{nonce}'"
    return response

if __name__ == '__main__':
    app.run(debug=True)

在这个例子中,静态样式使用哈希值进行验证,而动态样式使用 nonce 进行验证。CSP 策略中同时包含了哈希值和 nonce

高级用法:strict-dynamic

strict-dynamic 是 CSP Level 3 引入的一个非常有用的指令。它允许浏览器自动信任由受信任的脚本(例如,通过 nonce 或哈希值验证的脚本)动态插入的脚本和样式。这意味着,如果你使用 nonce 或哈希值验证了一个脚本,那么该脚本动态创建的任何其他脚本或样式也会被自动信任。

使用 strict-dynamic 可以简化 CSP 策略,并减少维护成本。

示例:

Content-Security-Policy: default-src 'self'; style-src 'self' 'nonce-{nonce}' 'strict-dynamic'; require-trusted-types-for 'script'; trusted-types default;

在这个例子中,我们使用了 strict-dynamic 指令,并且同时使用了 nonce。这意味着,任何具有正确 nonce 值的 <style> 标签以及由这些 <style> 标签动态创建的任何其他样式都会被信任。require-trusted-types-for 'script'; trusted-types default; 是 Trusted Types 的配置,和 strict-dynamic 配合使用可以进一步提升安全性。

浏览器兼容性:

  • nonce 和哈希值被大多数现代浏览器支持。
  • strict-dynamic 的支持度相对较低,需要根据你的目标用户群体进行评估。

总结

style-src 指令是 CSP 中用于控制 CSS 资源加载的关键部分。通过使用 nonce 和哈希值,你可以更精细地控制哪些内联样式可以被执行,从而大大提高 Web 应用的安全性。 选择 nonce 还是哈希值取决于你的具体需求,并始终遵循最佳实践,以确保你的 CSP 策略有效且易于维护。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注