跳转到主要内容

用于防范CSRF攻击的ASGI中间件

项目描述

asgi-csrf

PyPI Changelog codecov License

用于防范CSRF攻击的ASGI中间件

安装

pip install asgi-csrf

背景

请参阅OWASP关于跨站请求伪造(CSRF)的指南以及他们的跨站请求伪造(CSRF)预防技巧表

此中间件实现了双重提交cookie模式,其中设置一个cookie,然后将其与隐藏的csrftoken表单字段或x-csrftoken HTTP头进行比较。

用法

像这样装饰您的ASGI应用程序

from asgi_csrf import asgi_csrf
from .my_asgi_app import app


app = asgi_csrf(app, signing_secret="secret-goes-here")

如果缺少cookie,中间件将设置一个csrftoken cookie。该令牌的值将通过scope["csrftoken"]函数提供给您的ASGI应用程序。

您的应用程序代码应将此值作为隐藏表单字段包含在任何POST表单中

<form action="/login" method="POST">
    ...
    <input type="hidden" name="csrftoken" value="{{ request.scope.csrftoken() }}">
</form>

请注意,request.scope["csrftoken"]()是一个函数,返回一个字符串。调用该函数也使中间件知道如果用户尚未具有该cookie,该页面应该设置cookie。

如果需要设置cookie,中间件将在响应中添加一个Vary: Cookie头,以确保它不会被任何CDN或中间代理错误地缓存。

对于任何不包括匹配的csrftoken(无论是POST数据还是x-csrftoken HTTP头)的POST请求,中间件将返回403禁止错误(对于JavaScript fetch()调用非常有用)。

signing_secret用于签名令牌,以防止子域漏洞。

如果您没有传递显式的signing_secret参数,中间件将查找ASGI_CSRF_SECRET环境变量。

如果找不到该环境变量,它将生成一个随机密钥,该密钥将存在于服务器的整个生命周期中。

这意味着如果您未配置特定密钥,您用户的csrftokencookie将在服务器每次重启时都变为无效!您应该配置一个密钥。

如果尚未设置,则始终设置cookie

默认情况下,此中间件仅在用户遇到需要它的页面时才设置csrftokencookie - 例如,该页面调用request.scope["csrftoken"]()函数,以便在表单中填充隐藏字段。

如果您希望中间件为任何尚未提供cookie的传入请求设置该cookie,则可以使用always_set_cookie=True参数

app = asgi_csrf(app, signing_secret="secret-goes-here", always_set_cookie=True)

配置cookie

中间件可以通过多个选项配置来控制如何设置CSRFcookie

app = asgi_csrf(
    app,
    signing_secret="secret-goes-here",
    cookie_name="csrftoken",
    cookie_path="/",
    cookie_domain=None,
    cookie_secure=False,
    cookie_samesite="Lax"
)
  • cookie_name:要设置的cookie名称。默认为"csrftoken"
  • cookie_path:cookie有效的路径。默认为"/",表示cookie对整个域名有效。
  • cookie_domain:cookie有效的域名。默认为None,表示cookie仅对当前域名有效。
  • cookie_secure:如果设置为True,则cookie仅通过HTTPS连接发送。默认为False
  • cookie_samesite:控制cookie如何与跨站请求一起发送。可以是"Strict""Lax""None"。默认为"Lax"

跳过CSRF保护的其它情况

如果请求包含Authorization: Bearer ...标题,通常用于OAuth和JWT身份验证,则请求不需要包含CSRF令牌。这是因为浏览器无法在可能被滥用的上下文中发送这些标题。

如果请求没有任何cookie,则允许其通过,因为CSRF保护仅适用于来自已认证用户的请求。

always_protect

如果您有应该始终受保护且没有cookie的路径 - 例如您的登录表单(以避免登录CSRF攻击) - 您可以通过传递它们作为always_protect参数来保护这些路径

app = asgi_csrf(
    app,
    signing_secret="secret-goes-here",
    always_protect={"/login"}
)

skip_if_scope

可能存在您想要为认证POST请求禁用CSRF保护的情况 - 例如,对于Web API。

skip_if_scope=参数可以用于提供回调函数,该函数传递ASGI作用域并返回对于该请求是否跳过CSRF保护的True

此示例跳过了任何传入请求的CSRF保护,其中请求路径以/api/开头

def skip_api_paths(scope)
    return scope["path"].startswith("/api/")

app = asgi_csrf(
    app,
    signing_secret="secret-goes-here",
    skip_if_scope=skip_api_paths
)

使用send_csrf_failed自定义错误

默认情况下,当CSRF令牌缺失或无效时,中间件将返回一个包含简短错误消息的403禁止访问页面。

您可以通过传递一个send_csrf_failed函数到中间件来自定义此行为。此函数应接受ASGI的scopesend函数,以及发生错误的message_id

message_id将是一个表示asgi_csrf.Errors枚举中项的整数。

此示例展示了如何根据该message_id自定义错误消息

async def custom_csrf_failed(scope, send, message_id):
    assert scope["type"] == "http"
    await send(
        {
            "type": "http.response.start",
            "status": 403,
            "headers": [[b"content-type", b"text/html; charset=utf-8"]],
        }
    )
    await send(
        {
            "type": "http.response.body",
            "body": {
                Errors.FORM_URLENCODED_MISMATCH: "custom form-urlencoded error",
                Errors.MULTIPART_MISMATCH: "custom multipart error",
                Errors.FILE_BEFORE_TOKEN: "custom file before token error",
                Errors.UNKNOWN_CONTENT_TYPE: "custom unknown content type error",
            }
            .get(message_id, "")
            .encode("utf-8"),
        }
    )


app = asgi_csrf(
    app,
    signing_secret="secret-goes-here",
    send_csrf_failed=custom_csrf_failed
)

项目详情


下载文件

下载适用于您平台的文件。如果您不确定选择哪个,请了解更多关于安装包的信息。

源分发

asgi_csrf-0.10.tar.gz (11.2 kB 查看哈希值)

上传时间

构建分发

asgi_csrf-0.10-py3-none-any.whl (11.6 kB 查看哈希值)

上传时间 Python 3

由以下组织支持