Skip to content

Add manual CSP nonce and sha-256 support #163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 8, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ pkg/*
spike.rb
html
.idea
.ruby-version
.ruby-version
vendor/*
2 changes: 1 addition & 1 deletion lib/intercom-rails/auto_include_filter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def initialize(kontroller)
end

def include_javascript!
response.body = response.body.gsub(CLOSING_BODY_TAG, intercom_script_tag.output + '\\0')
response.body = response.body.gsub(CLOSING_BODY_TAG, intercom_script_tag.to_s + '\\0')
end

def include_javascript?
Expand Down
54 changes: 41 additions & 13 deletions lib/intercom-rails/script_tag.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,8 @@ class ScriptTag

include ::ActionView::Helpers::JavaScriptHelper

def self.generate(*args)
new(*args).output
end

attr_reader :user_details, :company_details, :show_everywhere
attr_accessor :secret, :widget_options, :controller
attr_accessor :secret, :widget_options, :controller, :nonce

def initialize(options = {})
self.secret = options[:secret] || Config.api_secret
Expand All @@ -27,13 +23,34 @@ def initialize(options = {})
elsif options[:user_details]
options[:user_details].delete(:company) if options[:user_details]
end
self.nonce = options[:nonce]
end

def valid?
valid = user_details[:app_id].present?
unless @show_everywhere
valid = valid && (user_details[:user_id] || user_details[:email]).present?
end
if nonce
valid = valid && valid_nonce?
end
valid
end

def valid_nonce?
valid = false
if nonce
# Base64 regexp:
# - blocks of 4 [A-Za-z0-9+/]
# followed either by:
# - blocks of 2 [A-Za-z0-9+/] + '=='
# - blocks of 3 [A-Za-z0-9+/] + '='
base64_regexp = Regexp.new('^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$')
m = base64_regexp.match(nonce)
if nonce == m.to_s
valid = true
end
end
valid
end

Expand All @@ -44,17 +61,28 @@ def intercom_settings
hsh
end

def output
def to_s
js_options = 'id="IntercomSettingsScriptTag"'
if nonce && valid_nonce?
js_options = js_options + " nonce=\"#{nonce}\""
end
str = "<script #{js_options}>#{intercom_javascript}</script>\n"
str.respond_to?(:html_safe) ? str.html_safe : str
end

def csp_sha256
base64_sha256 = Base64.encode64(Digest::SHA256.digest(intercom_javascript))
csp_hash = "sha256-#{base64_sha256}".delete("\n")
csp_hash
end

private
def intercom_javascript
intercom_settings_json = ActiveSupport::JSON.encode(intercom_settings).gsub('<', '\u003C')

str = <<-INTERCOM_SCRIPT
<script id="IntercomSettingsScriptTag">
window.intercomSettings = #{intercom_settings_json};
</script>
<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='#{Config.library_url || "https://widget.intercom.io/widget/#{j app_id}"}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}};})()</script>
INTERCOM_SCRIPT
str = "window.intercomSettings = #{intercom_settings_json};(function(){var w=window;var ic=w.Intercom;if(typeof ic===\"function\"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;s.src='#{Config.library_url || "https://widget.intercom.io/widget/#{j app_id}"}';var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}};})()"

str.respond_to?(:html_safe) ? str.html_safe : str
str
end

private
Expand Down
3 changes: 2 additions & 1 deletion lib/intercom-rails/script_tag_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module ScriptTagHelper
# @option user_details [Hash] :custom_data custom attributes you'd like saved for this user on Intercom.
# @option options [String] :widget a hash containing a css selector for an element which when clicked should show the Intercom widget
# @option options [String] :secret Your app secret for secure mode
# @option options [String] :nonce a nonce generated by your CSP framework to be included inside the javascript tag
# @return [String] Intercom script tag
# @example basic example
# <%= intercom_script_tag({ :app_id => "your-app-id",
Expand All @@ -33,7 +34,7 @@ def intercom_script_tag(user_details = nil, options={})
options[:find_current_user_details] = !options[:user_details]
options[:find_current_company_details] = !(options[:user_details] && options[:user_details][:company])
options[:controller] = controller if defined?(controller)
ScriptTag.generate(options)
ScriptTag.new(options)
end
end
end
10 changes: 5 additions & 5 deletions spec/auto_include_filter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,37 +94,37 @@ def current_user

it 'finds user from current_user method' do
get :with_current_user_method, :body => "<body>Hello world</body>"
expect(response.body).to include("<script>")
expect(response.body).to include('<script id="IntercomSettingsScriptTag">')
expect(response.body).to include("ciaran@intercom.io")
expect(response.body).to include("Ciaran Lee")
end

it 'finds user using config.user.current proc' do
IntercomRails.config.user.current = Proc.new { @admin }
get :with_admin_instance_variable, :body => "<body>Hello world</body>"
expect(response.body).to include("<script>")
expect(response.body).to include('<script id="IntercomSettingsScriptTag">')
expect(response.body).to include("eoghan@intercom.io")
expect(response.body).to include("Eoghan McCabe")
end

it 'excludes users if necessary' do
IntercomRails.config.user.exclude_if = Proc.new {|user| user.email.start_with?('ciaran')}
get :with_current_user_method, :body => "<body>Hello world</body>"
expect(response.body).not_to include("<script>")
expect(response.body).not_to include('<script id="IntercomSettingsScriptTag">')
expect(response.body).not_to include("ciaran@intercom.io")
expect(response.body).not_to include("Ciaran Lee")
end

it 'uses default library_url' do
get :with_current_user_method, :body => "<body>Hello world</body>"
expect(response.body).to include("<script>")
expect(response.body).to include('<script id="IntercomSettingsScriptTag">')
expect(response.body).to include("s.src='https://widget.intercom.io/widget/abc123'")
end

it 'allows library_url override' do
IntercomRails.config.library_url = 'http://a.b.c.d/library.js'
get :with_current_user_method, :body => "<body>Hello world</body>"
expect(response.body).to include("<script>")
expect(response.body).to include('<script id="IntercomSettingsScriptTag">')
expect(response.body).to include("s.src='http://a.b.c.d/library.js")
end

Expand Down
29 changes: 27 additions & 2 deletions spec/script_tag_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
include IntercomRails::ScriptTagHelper

it 'delegates to script tag ' do
expect(IntercomRails::ScriptTag).to receive(:generate)
expect(IntercomRails::ScriptTag).to receive(:new)
intercom_script_tag({})
end

it 'does not use dummy data if app_id is set in development' do
allow(Rails).to receive(:development?).and_return true
output = intercom_script_tag({app_id: 'thisismyappid', email:'foo'})
output = intercom_script_tag({app_id: 'thisismyappid', email:'foo'}).to_s
expect(output).to include("/widget/thisismyappid")
end

Expand All @@ -24,4 +24,29 @@
fake_action_view.intercom_script_tag({})
expect(obj.instance_variable_get(IntercomRails::SCRIPT_TAG_HELPER_CALLED_INSTANCE_VARIABLE)).to eq(true)
end

context 'content security policy support' do
it 'returns a valid sha256 hash for the CSP header' do
#
# See also spec/script_tag_spec.rb
#
script_tag = intercom_script_tag({
:app_id => 'csp_sha_test',
:email => 'marco@intercom.io',
:user_id => 'marco',
})
expect(script_tag.csp_sha256).to eq('sha256-qLRbekKD6dEDMyLKPNFYpokzwYCz+WeNPqJE603mT24=')
end

it 'inserts a valid nonce if present' do
script_tag = intercom_script_tag({
:app_id => 'csp_sha_test',
:email => 'marco@intercom.io',
:user_id => 'marco',
}, {
:nonce => 'pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94=',
})
expect(script_tag.to_s).to include('nonce="pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94="')
end
end
end
47 changes: 44 additions & 3 deletions spec/script_tag_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
end

it 'should output html_safe?' do
expect(ScriptTag.generate({}).html_safe?).to be(true)
expect(ScriptTag.new({}).to_s.html_safe?).to be(true)
end

it 'should convert times to unix timestamps' do
Expand Down Expand Up @@ -44,7 +44,7 @@
it 'should escape html attributes' do
nasty_email = "</script><script>alert('sup?');</script>"
script_tag = ScriptTag.new(:user_details => {:email => nasty_email})
expect(script_tag.output).not_to include(nasty_email)
expect(script_tag.to_s).not_to include(nasty_email)
end

it 'should escape html attributes in app_id' do
Expand All @@ -53,7 +53,7 @@
nasty_app_id = "</script><script>alert('sup?');</script>"
IntercomRails.config.app_id = nasty_app_id
script_tag = ScriptTag.new(:user_details => {:email => email})
expect(script_tag.output).not_to include(nasty_app_id)
expect(script_tag.to_s).not_to include(nasty_app_id)
IntercomRails.config.app_id = before
end

Expand Down Expand Up @@ -142,4 +142,45 @@ def sha256_hmac(secret, input)
expect(script_tag.valid?).to eq(false)
end
end

context 'content security policy support' do
it 'returns a valid sha256 hash for the CSP header' do
#
# If default values change, re-generate the string below using this one
# liner:
# echo "sha256-$(echo -n "js code" | openssl dgst -sha256 -binary | openssl base64)"
# or an online service like https://report-uri.io/home/hash/
#
# For instance:
# echo "sha256-$(echo -n "alert('hello');" | openssl dgst -sha256 -binary | openssl base64)"
# sha256-gj4FLpwFgWrJxA7NLcFCWSwEF/PMnmWidszB6OONAAo=
#
script_tag = ScriptTag.new(:user_details => {
:app_id => 'csp_sha_test',
:email => 'marco@intercom.io',
:user_id => 'marco',
})
expect(script_tag.csp_sha256).to eq('sha256-qLRbekKD6dEDMyLKPNFYpokzwYCz+WeNPqJE603mT24=')
end

it 'inserts a valid nonce if present' do
script_tag = ScriptTag.new(:user_details => {
:app_id => 'csp_sha_test',
:email => 'marco@intercom.io',
:user_id => 'marco',
},
:nonce => 'pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94=')
expect(script_tag.to_s).to include('nonce="pJwtLVnwiMaPCxpb41KZguOcC5mGUYD+8RNGcJSlR94="')
end

it 'does not insert a nasty nonce if present' do
script_tag = ScriptTag.new(:user_details => {
:app_id => 'csp_sha_test',
:email => 'marco@intercom.io',
:user_id => 'marco',
},
:nonce => '>alert(1)</script><script>')
expect(script_tag.to_s).not_to include('>alert(1)</script><script>')
end
end
end