Vapor Security Headers

Language Build Status Code Coverage MIT License

一个中间件库,用于向你的 Vapor 应用程序添加安全标头。

特性

轻松地将标头添加到你的所有响应中,以提高你和你的用户的站点安全性。目前支持

这些标头将帮助防止跨站点脚本攻击、SSL 降级攻击、内容注入攻击、点击劫持等。它们不会帮助直接针对你的服务器的任何攻击,但它们将帮助你的用户并帮助保护敏感信息(CSRF 令牌)。请注意,此库不保证任何内容,并且没有任何内容是完全安全的。

用法

添加包

将包作为依赖项添加到你的 Package.swift 清单中

dependencies: [
    ...,
    .package(url: "https://github.com/brokenhandsio/VaporSecurityHeaders.git", from: "3.0.0")
]

然后将依赖项添加到你的目标中

.target(name: "App",
        dependencies: [
                // ...
                "VaporSecurityHeaders"]),

配置

要使用 Vapor Security Headers,你需要将中间件添加到你的 ApplicationMiddlewares 中。 Vapor Security Headers 通过工厂上的 build 函数可以轻松实现这一点。 注意:如果你希望将安全标头添加到错误响应(推荐),你需要从头开始初始化 Middlewares,并在 SecuriyHeaders 之后 添加中间件。在 configure.swift 中添加

let securityHeadersFactory = SecurityHeadersFactory()

application.middleware = Middlewares()
application.middleware.use(securityHeadersFactory.build())
application.middleware.use(ErrorMiddleware.default(environment: application.environment))
// Add other middlewares...

默认工厂会将默认值添加到你的站点的 Content-Security-Policy、X-XSS-Protection、X-Frame-Options 和 X-Content-Type-Options 中。

x-content-type-options: nosniff
content-security-policy: default-src 'self'
x-frame-options: DENY
x-xss-protection: 0

注意:你应该确保将安全标头设置为 Middlewares 中的第一个中间件(即,第一个应用于响应的中间件),以确保将标头添加到所有响应中。

如果你想添加自己的值,使用工厂很容易实现。 例如,要添加内容安全策略配置,只需执行以下操作

let cspValue = "default-src 'none'; script-src https://static.brokenhands.io;"

let cspConfig = ContentSecurityPolicyConfiguration(value: cspValue)

let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
application.middleware.use(securityHeadersFactory.build())
x-content-type-options: nosniff
content-security-policy: default-src 'none'; script-src https://static.brokenhands.io;
x-frame-options: DENY
x-xss-protection: 0

每个不同的标头都有其自己的配置和选项,详细信息可以在下面找到。

你可以访问优秀的 Security Headers(无隶属关系)网站来测试你的站点。

API 标头

如果你正在运行 API,你可以通过使用以下代码创建默认配置:

let securityHeaders = SecurityHeadersFactory.api()
application.middleware.use(securityHeaders.build())
x-content-type-options: nosniff
content-security-policy: default-src 'none'
x-frame-options: DENY
x-xss-protection: 0

服务器配置

Vapor

如果你自己运行 Vapor(即,不作为 CGI 应用程序或在反向代理之后),那么你不需要做任何事情就可以运行它!

Nginx、Apache 和第三方服务

当作为反向代理运行时,这两个 Web 服务器都应该毫无问题地传递 Vapor 的响应标头。 某些服务器和提供商(例如 Heroku)会注入自己的标头或阻止某些标头(例如 HSTS,以阻止你锁定其整个站点)。 你需要与你的提供商核实以查看启用了哪些功能和允许的功能。

安全标头信息

内容安全策略 (Content-Security-Policy)

内容安全策略是防止跨站点脚本攻击的最有效工具之一。 本质上,它是一种为内容来源建立白名单的方法,以便你仅从已知和受信任的来源加载内容。 有关 CSP 的更多信息,请阅读 Scott Helme 的 精彩的博客文章,其中介绍了如何配置它以及使用什么。

Vapor Security Headers 包将设置 default-src: 'self' 的默认 CSP,这意味着你可以从你的域加载图像、脚本、字体、CSS 等。 这也意味着你不能有任何内联 Javascript 或 CSS,这是你可以采取的保护你的站点的最有效措施之一,并且会消除很大一部分内容注入攻击。

API 默认 CSP 是 default-src: 'none',因为 API 应该只返回数据,而永远不应该加载脚本或图像来显示!

你可以使用以下指令构建 CSP 标头 (ContentSecurityPolicy)

示例

let cspConfig = ContentSecurityPolicy()
        .scriptSrc(sources: "https://static.brokenhands.io")
        .styleSrc(sources: "https://static.brokenhands.io")
        .imgSrc(sources: "https://static.brokenhands.io")
Content-Security-Policy: script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io

你可以使用 ContentSecurityPolicy().set(value) 或 ContentSecurityPolicyConfiguration(value) 设置自定义标头。

ContentSecurityPolicy().set(value)

let cspBuilder = ContentSecurityPolicy().set(value: "default-src: 'none'")

let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)

let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)

ContentSecurityPolicyConfiguration(value)

let cspConfig = ContentSecurityPolicyConfiguration(value: "default-src 'none'")

let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
Content-Security-Policy: default-src: 'none'

以下 CSP 关键字 (CSPKeywords) 也可供你使用

示例

CSPKeywords.`self` // “‘self’”
ContentSecurityPolicy().defaultSrc(sources: CSPKeywords.`self`)
Content-Security-Policy: default-src 'self'

你还可以利用 Report-To 指令

let reportToEndpoint = CSPReportToEndpoint(url: "https://csp-report.brokenhands.io/csp-reports")

let reportToValue = CSPReportTo(group: "vapor-csp", max_age: 10886400, endpoints: [reportToEndpoint], include_subdomains: true)

let cspValue = ContentSecurityPolicy()
    .defaultSrc(sources: CSPKeywords.none)
    .scriptSrc(sources: "https://static.brokenhands.io")
    .reportTo(reportToObject: reportToValue)
Content-Security-Policy: default-src 'none'; script-src https://static.brokenhands.io; report-to {"group":"vapor-csp","endpoints":[{"url":"https:\/\/csp-report.brokenhands.io\/csp-reports"}],"include_subdomains":true,"max_age":10886400}

有关 Report-To 指令的更多信息,请参阅 Google Developers - The Reporting API

内容安全策略配置

要配置你的 CSP,你可以将其添加到你的 ContentSecurityPolicyConfiguration 中,如下所示

let cspBuilder = ContentSecurityPolicy()
    .defaultSrc(sources: CSPKeywords.none)
    .scriptSrc(sources: "https://static.brokenhands.io")
    .styleSrc(sources: "https://static.brokenhands.io")
    .imgSrc(sources: "https://static.brokenhands.io")
    .fontSrc(sources: "https://static.brokenhands.io")
    .connectSrc(sources: "https://*.brokenhands.io")
    .formAction(sources: CSPKeywords.`self`)
    .upgradeInsecureRequests()
    .blockAllMixedContent()
    .requireSriFor(values: "script", "style")
    .reportUri(uri: "https://csp-report.brokenhands.io")

let cspConfig = ContentSecurityPolicyConfiguration(value: cspBuilder)

let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicy: cspConfig)
Content-Security-Policy: default-src 'none'; script-src https://static.brokenhands.io; style-src https://static.brokenhands.io; img-src https://static.brokenhands.io; font-src https://static.brokenhands.io; connect-src https://*.brokenhands.io; form-action 'self'; upgrade-insecure-requests; block-all-mixed-content; require-sri-for script style; report-uri https://csp-report.brokenhands.io

此策略意味着默认情况下所有内容都被阻止,但是

查看 https://report-uri.io/ 以获取一个免费工具,用于将你的所有 CSP 报告发送到该工具。

页面特定 CSP

Vapor Security Headers 还支持在路由或请求基础上设置 CSP。 如果已将中间件添加到 Middlewares,你可以覆盖请求的 CSP。 这允许你拥有严格的默认 CSP,但在需要时允许来自额外来源的内容,例如仅允许博客页面上的博客评论的 Javascript。 创建一个单独的 ContentSecurityPolicyConfiguration,然后将其添加到请求中。 例如,在路由处理程序中,你可以执行

let cspConfig = ContentSecurityPolicy()
    .defaultSrc(sources: CSPKeywords.none)
    .scriptSrc(sources: "https://comments.disqus.com")

let pageSpecificCSP = ContentSecurityPolicyConfiguration(value: cspConfig)
req.contentSecurityPolicy = pageSpecificCSP
content-security-policy: default-src 'none'; script-src https://comments.disqus.com

内容安全策略报告模式 (Content-Security-Policy-Report-Only)

Content-Security-Policy-Report-Only 的工作方式与 Content-Security-Policy 完全相同,除了任何违规行为都不会阻止内容,但它们会报告给你。 这对于在你的站点上推出 CSP 之前测试它非常有用。 你可以并排运行两者 - 例如,在 Content-Security-Policy 下具有相当简单的策略,但在 Content-Security-Policy-Report-Only 上测试更严格的策略。 这样做的好处是你的用户为你完成所有测试!

要配置此项,只需将你的策略传递到 ContentSecurityPolicyReportOnlyConfiguration

let cspConfig = ContentSecurityPolicyReportOnlyConfiguration(value: "default-src https:; report-uri https://csp-report.brokenhands.io")
        
let securityHeadersFactory = SecurityHeadersFactory().with(contentSecurityPolicyReportOnly: cspConfig)  
content-security-policy-report-only: default-src https:; report-uri https://csp-report.brokenhands.io

上面的博客文章对此进行了更详细的介绍。

X-XSS-Protection

X-XSS-Protection 配置浏览器的跨站点脚本过滤器。 此包配置为禁用标头,这(令人惊讶地)提供了安全优势。 有关更多信息,请参阅 MDN 上的这篇文章

let xssProtectionConfig = XSSProtectionConfiguration()
    
let securityHeadersFactory = SecurityHeadersFactory().with(XSSProtection: xssProtectionConfig)
x-xss-protection: 0

X-Content-Type-Options

X-Content-Type-Options 阻止浏览器尝试从请求中嗅探 MIME 内容类型,并确保使用声明的内容类型。 它只有一个选项,即 nosniff。 要使用此功能,请按如下所示设置你的 ContentTypeOptionsConfiguration(这在任何 SecurityHeaders 对象上默认设置)

let contentTypeConfig = ContentTypeOptionsConfiguration(option: .nosniff)
    
let securityHeadersFactory = SecurityHeadersFactory().with(contentTypeOptions: contentTypeConfig)
x-content-type-options: nosniff

要禁用它

let contentTypeConfig = ContentTypeOptionsConfiguration(option: .none)

X-Frame-Options

X-Frame-Options 标头用于点击劫持攻击,并告诉浏览器你的站点是否可以被框定。 要完全阻止你的站点被框定(默认设置)

let frameOptionsConfig = FrameOptionsConfiguration(option: .deny)

let securityHeadersFactory = SecurityHeadersFactory().with(frameOptions: frameOptionsConfig)
x-frame-options: DENY

要允许你框定你自己的站点

let frameOptionsConfig = FrameOptionsConfiguration(option: .sameOrigin)
x-frame-options: SAMEORIGIN

要允许特定站点框定你的站点,请使用

let frameOptionsConfig = FrameOptionsConfiguration(option: .allow(from: "https://mytrustedsite.com"))
x-frame-options: ALLOW-FROM https://mytrustedsite.com

严格传输安全

严格传输安全是对 301/302 重定向或 HTTPS 转发的改进。 当你导航到某个地址时,浏览器将默认为 HTTP,但 HSTS(HTTP 严格传输安全)告诉浏览器它应该始终通过 HTTPS 连接,因此所有未来的请求都将是 HTTPS,即使你单击 HTTP 链接。 默认情况下,Security Headers 库未启用此功能,因为它可能会在你未正确设置 HTTPS 的情况下导致问题。 如果你指定此标头,然后在未来的某个日期你未续订你的 SSL 证书或禁用 SSL,则浏览器将拒绝加载你的站点! 但是,强烈建议使用它,因为它确保所有连接都通过 HTTPS,即使是用用户单击 HTTP 链接。

默认配置为 max-age=31536000; includeSubDomains; preload。 这告诉浏览器强制执行 HTTPS 一年,并且对于每个子域也是如此。 因此,如果你指定此项,请确保你已为所有子域正确配置了 SSL,例如 test.mysite.comdev.mysite.com 等。

preload 标记告诉 Chrome 你想要被预加载。 这会将你添加到预加载列表中,这意味着浏览器会自动知道你想要 HTTPS 连接,甚至在你访问该站点之前,因此会删除你首次指定标头的初始 HTTP 握手。 但是,这现在已经被取代,你现在应该在 https://hstspreload.org 提交你的站点。 这会将你的站点添加到 Chrome 的源代码中以供将来预加载,并且它是其他浏览器也使用的列表。 请注意,很难从列表中删除自己(并且可能需要数月才能将其推广到浏览器),因此通过提交你的站点,你实际上保证了你站点的剩余生命周期的 HTTPS 工作。 但是,如今这不应该是一个问题 - 使用 Let's Encrypt注意:你应该小心在 Heroku 等部署站点上使用此功能,因为它可能会导致问题。

要使用 Strict-Transport-Security 标头,您可以对其进行配置并按如下方式添加(显示的是默认值):

let strictTransportSecurityConfig = StrictTransportSecurityConfiguration(maxAge: 31536000, includeSubdomains: true, preload: true)

let securityHeadersFactory = SecurityHeadersFactory().with(strictTransportSecurity: strictTransportSecurityConfig)
strict-transport-security: max-age=31536000; includeSubDomains; preload

将 HTTP 重定向到 HTTPS

如果 Strict-Transport-Security 不足以实现浏览器到 HTTPS 的转发连接,您可以选择添加一个额外的中间件,以便在客户端尝试通过 HTTP 连接访问您的站点时提供此重定向。

要使用 HTTPS 重定向中间件,您可以在 configure.swift 中添加以下行以启用该中间件。 这必须在 securityHeadersFactory.build() 之前完成,以确保 HSTS 正常工作。

app.middleware.use(HTTPSRedirectMiddleware())

HTTPSRedirectMiddleware 允许您设置应用程序可以重定向到的允许主机数组。 这可以防止攻击者破坏 Host 标头并强制重定向到他们控制下的域名。 要使用此功能,请将允许主机列表提供给初始化程序。

app.middleware.use(HTTPSRedirectMiddleware(allowedHosts: ["www.brokenhands.io", "brokenhands.io", "static.brokenhands.io"))

任何尝试重定向到另一个主机(例如 attacker.com)的行为都将导致 400 Bad Request 响应。

Server

通常会从响应中隐藏 Server 标头,以防止泄露您正在运行的服务器类型及其版本。 这样做是为了阻止攻击者扫描您的站点并轻易地利用已知的漏洞。 默认情况下,Vapor 不会在响应中显示服务器标头,原因就在于此。

但是,添加自定义服务器配置以实现一些个性化(例如您的网站名称或公司名称)(请参阅 Github 的响应),并使用 ServerConfiguraiton 实现此目的可能会很有趣。 因此,例如,如果我希望我的 Server 标头为 brokenhands.io,我会像这样配置它:

let serverConfig = ServerConfiguration(value: "brokenhands.io")

let securityHeadersFactory = SecurityHeadersFactory().with(server: serverConfig)
server: brokenhands.io

Referrer Policy (引用站点策略)

Referrer Policy 是最新引入的标头(规范可以在这里找到)。 它基本上定义了何时可以通过请求发送 Referrer 标头。 例如,您可能不想在从 HTTPS 转到 HTTP 时发送标头。

不同的选项包括:

我不会详细介绍每一个选项,我会将您指向 Scott Helme 的一个更好的解释:点击这里

let referrerPolicyConfig = ReferrerPolicyConfiguration(.noReferrer)

let securityHeadersFactory = SecurityHeadersFactory().with(referrerPolicy: referrerPolicyConfig)
referrer-policy: no-referrer

您还可以设置回退策略

let referrerPolicyConfig = ReferrerPolicyConfiguration([.noReferrer, .strictOriginWhenCrossOrigin])

let securityHeadersFactory = SecurityHeadersFactory().with(referrerPolicy: referrerPolicyConfig)
referrer-policy: no-referrer, strict-origin-when-cross-origin