1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171
|
# frozen_string_literal: true
module SecureHeaders
module ViewHelpers
include SecureHeaders::HashHelper
SECURE_HEADERS_RAKE_TASK = "rake secure_headers:generate_hashes"
class UnexpectedHashedScriptException < StandardError; end
# Public: create a style tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to style-src directive.
#
# Returns an html-safe style tag with the nonce attribute.
def nonced_style_tag(content_or_options = {}, &block)
nonced_tag(:style, content_or_options, block)
end
# Public: create a stylesheet link tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to style-src directive.
#
# Returns an html-safe link tag with the nonce attribute.
def nonced_stylesheet_link_tag(*args, &block)
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
stylesheet_link_tag(*args, **opts, &block)
end
# Public: create a script tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to script-src directive.
#
# Returns an html-safe script tag with the nonce attribute.
def nonced_javascript_tag(content_or_options = {}, &block)
nonced_tag(:script, content_or_options, block)
end
# Public: create a script src tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to script-src directive.
#
# Returns an html-safe script tag with the nonce attribute.
def nonced_javascript_include_tag(*args, &block)
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
javascript_include_tag(*args, **opts, &block)
end
# Public: create a script Webpacker pack tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to script-src directive.
#
# Returns an html-safe script tag with the nonce attribute.
def nonced_javascript_pack_tag(*args, &block)
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:script))
javascript_pack_tag(*args, **opts, &block)
end
# Public: create a stylesheet Webpacker link tag using the content security policy nonce.
# Instructs secure_headers to append a nonce to style-src directive.
#
# Returns an html-safe link tag with the nonce attribute.
def nonced_stylesheet_pack_tag(*args, &block)
opts = extract_options(args).merge(nonce: _content_security_policy_nonce(:style))
stylesheet_pack_tag(*args, **opts, &block)
end
# Public: use the content security policy nonce for this request directly.
# Instructs secure_headers to append a nonce to style/script-src directives.
#
# Returns a non-html-safe nonce value.
def _content_security_policy_nonce(type)
case type
when :script
SecureHeaders.content_security_policy_script_nonce(@_request)
when :style
SecureHeaders.content_security_policy_style_nonce(@_request)
end
end
alias_method :content_security_policy_nonce, :_content_security_policy_nonce
def content_security_policy_script_nonce
_content_security_policy_nonce(:script)
end
def content_security_policy_style_nonce
_content_security_policy_nonce(:style)
end
##
# Checks to see if the hashed code is expected and adds the hash source
# value to the current CSP.
#
# By default, in development/test/etc. an exception will be raised.
def hashed_javascript_tag(raise_error_on_unrecognized_hash = nil, &block)
hashed_tag(
:script,
:script_src,
Configuration.instance_variable_get(:@script_hashes),
raise_error_on_unrecognized_hash,
block
)
end
def hashed_style_tag(raise_error_on_unrecognized_hash = nil, &block)
hashed_tag(
:style,
:style_src,
Configuration.instance_variable_get(:@style_hashes),
raise_error_on_unrecognized_hash,
block
)
end
private
def hashed_tag(type, directive, hashes, raise_error_on_unrecognized_hash, block)
if raise_error_on_unrecognized_hash.nil?
raise_error_on_unrecognized_hash = ENV["RAILS_ENV"] != "production"
end
content = capture(&block)
file_path = File.join("app", "views", self.instance_variable_get(:@virtual_path) + ".html.erb")
if raise_error_on_unrecognized_hash
hash_value = hash_source(content)
message = unexpected_hash_error_message(file_path, content, hash_value)
if hashes.nil? || hashes[file_path].nil? || !hashes[file_path].include?(hash_value)
raise UnexpectedHashedScriptException.new(message)
end
end
SecureHeaders.append_content_security_policy_directives(request, directive => hashes[file_path])
content_tag type, content
end
def unexpected_hash_error_message(file_path, content, hash_value)
<<-EOF
\n\n*** WARNING: Unrecognized hash in #{file_path}!!! Value: #{hash_value} ***
#{content}
*** Run #{SECURE_HEADERS_RAKE_TASK} or add the following to config/secure_headers_generated_hashes.yml:***
#{file_path}:
- \"#{hash_value}\"\n\n
NOTE: dynamic javascript is not supported using script hash integration
on purpose. It defeats the point of using it in the first place.
EOF
end
def nonced_tag(type, content_or_options, block)
options = {}
content = if block
options = content_or_options
capture(&block)
else
content_or_options.html_safe # :'(
end
content_tag type, content, options.merge(nonce: _content_security_policy_nonce(type))
end
def extract_options(args)
if args.last.is_a? Hash
args.pop
else
{}
end
end
end
end
ActiveSupport.on_load :action_view do
include SecureHeaders::ViewHelpers
end if defined?(ActiveSupport)
|