# frozen-string-literal: true#classRodamoduleRodaPlugins# The content_security_policy plugin allows you to easily set a Content-Security-Policy# header for the application, which modern browsers will use to control access to specific# types of page content.## You would generally call the plugin with a block to set the default policy:## plugin :content_security_policy do |csp|# csp.default_src :none# csp.img_src :self# csp.style_src :self# csp.script_src :self# csp.font_src :self# csp.form_action :self# csp.base_uri :none# csp.frame_ancestors :none# csp.block_all_mixed_content# end## Then, anywhere in the routing tree, you can customize the policy for just that# branch or action using the same block syntax:## r.get 'foo' do# content_security_policy do |csp|# csp.object_src :self# csp.add_style_src 'bar.com'# end# # ...# end## In addition to using a block, you can also call methods on the object returned# by the method:## r.get 'foo' do# content_security_policy.script_src :self, 'example.com', [:nonce, 'foobarbaz']# # ...# end## The following methods are available for configuring the content security policy,# which specify the setting (substituting _ with -): ## * base_uri# * child_src# * connect_src# * default_src# * font_src# * form_action# * frame_ancestors# * frame_src# * img_src# * manifest_src# * media_src# * object_src# * plugin_types# * report_to# * report_uri# * require_sri_for# * sandbox# * script_src# * style_src# * worker_src## All of these methods support any number of arguments, and each argument should# be one of the following types:## String :: used verbatim# Symbol :: Substitutes +_+ with +-+ and surrounds with <tt>'</tt># Array :: only accepts 2 element arrays, joins elements with +-+ and# surrounds the result with <tt>'</tt>## Example:## content_security_policy.script_src :self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']# # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; # # When calling a method with no arguments, the setting is removed from the policy instead# of being left empty, since all of these setting require at least one value. Likewise,# if the policy does not have any settings, the header will not be added.## Calling the method overrides any previous setting. Each of the methods has +add_*+ and# +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method# returns the current value for the setting.## content_security_policy.script_src :self, :unsafe_eval# content_security_policy.add_script_src 'example.com', [:nonce, 'foobarbaz']# # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; ## content_security_policy.get_script_src# # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']]## The clear method can be used to remove all settings from the policy. Empty policies# do not set any headers. You can use +response.skip_content_security_policy!+ to skip# setting a policy. This is faster than calling +content_security_policy.clear+, since# it does not duplicate the default policy.## The following methods to set boolean settings are also defined:## * block_all_mixed_content# * upgrade_insecure_requests## Calling these methods will turn on the related setting. To turn the setting# off again, you can call them with a +false+ argument. There is also a <tt>*?</tt> method# for each setting for returning whether the setting is currently enabled.## Likewise there is also a +report_only+ method for turning on report only mode (the# default is enforcement mode), or turning off report only mode if a false argument# is given. Also, there is a +report_only?+ method for returning whether report only# mode is enabled.moduleContentSecurityPolicy# Represents a content security policy.classPolicy'
base-uri
child-src
connect-src
default-src
font-src
form-action
frame-ancestors
frame-src
img-src
manifest-src
media-src
object-src
plugin-types
report-to
report-uri
require-sri-for
sandbox
script-src
style-src
worker-src
'.split.each(&:freeze).eachdo|setting|meth=setting.gsub('-','_').freeze# Setting method name sets the setting value, or removes it if no args are given.define_method(meth)do|*args|ifargs.empty?@opts.delete(setting)else@opts[setting]=args.freezeendnilend# add_* method name adds to the setting value, or clears setting if no values# are given.define_method("add_#{meth}")do|*args|unlessargs.empty?@opts[setting]||=EMPTY_ARRAY@opts[setting]+=args@opts[setting].freezeendnilend# get_* method always returns current setting value.define_method("get_#{meth}")do@opts[setting]endend%w'block-all-mixed-content upgrade-insecure-requests'.each(&:freeze).eachdo|setting|meth=setting.gsub('-','_').freeze# Setting method name turns on setting if true or no argument given,# or removes setting if false is given.define_method(meth)do|arg=true|ifarg@opts[setting]=trueelse@opts.delete(setting)endnilend# *? method returns true or false depending on whether setting is enabled.define_method("#{meth}?")do!!@opts[setting]endenddefinitializeclearend# Clear all settings, useful to remove any inherited settings.defclear@opts={}end# Do not allow future modifications to any settings.deffreeze@opts.freezeheader_value.freezesuperend# The header name to use, depends on whether report only mode has been enabled.defheader_key@report_only?RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY:RodaResponseHeaders::CONTENT_SECURITY_POLICYend# The header value to use.defheader_valuereturn@header_valueif@header_values=String.new@opts.eachdo|k,vs|s<<kunlessvs==truevs.each{|v|append_formatted_value(s,v)}ends<<'; 'end@header_value=send# Set whether the Content-Security-Policy-Report-Only header instead of the# default Content-Security-Policy header.defreport_only(report=true)@report_only=reportend# Whether this policy uses report only mode.defreport_only?!!@report_onlyend# Set the current policy in the headers hash. If no settings have been made# in the policy, does not set a header.defset_header(headers)returnif@opts.empty?headers[header_key]||=header_valueendprivate# Handle three types of values when formatting the header:# String :: used verbatim# Symbol :: Substitutes _ with - and surrounds with '# Array :: only accepts 2 element arrays, joins them with - and# surrounds them with 'defappend_formatted_value(s,v)casevwhenStrings<<' '<<vwhenArraycasev.lengthwhen2s<<" '"<<v.join('-')<<"'"elseraiseRodaError,"unsupported CSP value used: #{v.inspect}"endwhenSymbols<<" '"<<v.to_s.gsub('_','-')<<"'"elseraiseRodaError,"unsupported CSP value used: #{v.inspect}"endend# Make object copy use copy of settings, and remove cached header value.definitialize_copy(_)super@opts=@opts.dup@header_value=nilendend# Yield the current Content Security Policy to the block.defself.configure(app)policy=app.opts[:content_security_policy]=ifpolicy=app.opts[:content_security_policy]policy.dupelsePolicy.newendyieldpolicyifdefined?(yield)policy.freezeendmoduleInstanceMethods# If a block is given, yield the current content security policy. Returns the# current content security policy.defcontent_security_policypolicy=@_response.content_security_policyyieldpolicyifdefined?(yield)policyendendmoduleResponseMethods# Unset any content security policy when reinitializingdefinitializesuper@content_security_policy&&=nilend# The current content security policy to be used for this response.defcontent_security_policy@content_security_policy||=roda_class.opts[:content_security_policy].dupend# Do not set a content security policy header for this response.defskip_content_security_policy!@skip_content_security_policy=trueendprivate# Set the appropriate content security policy header.defset_default_headerssuperunless@skip_content_security_policy(@content_security_policy||roda_class.opts[:content_security_policy]).set_header(headers)endendendendregister_plugin(:content_security_policy,ContentSecurityPolicy)endend