0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-04 10:39:43 +08:00
discourse/lib/middleware/csp_script_nonce_injector.rb
Alan Guo Xiang Tan 661ea0c2bd
PERF: Avoid replacing CSP nonces in non-cacheable responses (#42008)
CSP nonce placeholders are replaced after rendering by scanning and
copying the complete response body, even when the response could not
enter the anonymous cache.

Generate the final nonce before rendering non-cacheable requests so
templates can emit it directly. Requests eligible for anonymous caching
continue to use placeholders, preserving a unique nonce each time cached
HTML is served.

On a site with 11K categories for a logged in user, ~400ms was spent
executing `gsub` on the response body.
2026-07-24 09:49:01 +08:00

32 lines
946 B
Ruby
Vendored

# frozen_string_literal: true
module Middleware
class CspScriptNonceInjector
NONCE_ENV = "discourse.csp_nonce"
PLACEHOLDER_HEADER = "Discourse-CSP-Nonce-Placeholder"
def initialize(app, settings = {})
@app = app
end
def call(env)
status, headers, response = @app.call(env)
if nonce_placeholder = headers.delete(PLACEHOLDER_HEADER)
nonce = env[NONCE_ENV] || SecureRandom.alphanumeric(25)
parts = response
if !env[NONCE_ENV]
parts = []
response.each { |part| parts << part.to_s.gsub(nonce_placeholder, nonce) }
end
%w[Content-Security-Policy Content-Security-Policy-Report-Only].each do |name|
next if headers[name].blank?
headers[name] = headers[name].sub("script-src ", "script-src 'nonce-#{nonce}' ")
end
[status, headers, parts]
else
[status, headers, response]
end
end
end
end