Skip to content

Commit

Permalink
Adds SRI integrity attribute to template
Browse files Browse the repository at this point in the history
How to test
-----------

In the application using govuk_template, you will have to set `config.assets.debug = false` in config/environments/development.rb or run the application in production mode.
This is because sprockets-rails checks for this flag when deciding if it should calculate the integrity attribute: https://github.com/rails/sprockets-rails/blob/10bc1bd096b39a3dd632571dd517788314657056/lib/sprockets/rails/helper.rb#L168

What is SRI
-----------
SRI will add an integrity attribute on your script tags:

<script src="https://example.com/example.css"
integrity_no="sha384oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous"></script>

The example above is generated automatically by sprockets-rails in your project if you do this:
<%= stylesheet_script_tag 'example', integrity: true %>

The way the digest value for the integrity attribute is calculated is by applying SHA256 on the contents of the file. In that way, the browser can double check that the right file is being received and hasn’t been tampered with (which would mean the contents would have changed and the resulting digest would be incorrect).

The problem with using this in govuk_template
---------------------------------------------
The css files it provided by govuk_template (for example: to the static project), are still css.erb files. In which case the contents of those files will change when they are processed in static (specifically there’s multiple <%= asset_path(...)%>s in there).

So here’s the problem illustrated:

1. The govuk_template gem compiles a file called govuk_template.css.erb which contains multiple asset_path lines, e.g.
background-image: url(<%= asset_path 'images/govuk-crest.png' %>);

2. static receives this file and replaces all the asset_path lines with it’s own path, e.g.
background-image: url(**http://static.dev.gov.uk/static**/images/govuk-crest-2x.png);
Notice that this url will vary depending on the app that uses the gem
=> this means the contents of the resulting govuk_template.css will vary depending on which app is using the gem so the digest will also vary.
This means you can’t calculate the digest inside the gem, and will have to be left to the apps to do it individually

3. You cannot use sprockets-rails in govuk_template directly because the assets it serves are also compiled into django, play, liquid, mustache and other crazy formats.
So you can’t do this:
<%= stylesheet_link_tag ‘govuk-template-print’, integrity: true%>
and then leave it up to sprockets-rails to calculate the digest for you, because this will not work for django and all the other formats. They don’t know what stylesheet_tag is.
Instead, this is what govuk_template does so it can be compiled into multiple languages:
<link href="<%= asset_path "govuk-template-print.css" %>" media="print" rel="stylesheet" />

4. You cannot calculate the integrity digest by hand in the gem, because of point 2 -> the content will change

Proposed solution
-----------------
We use stylesheet_link_tag and set integrity: true, but we leave it up to the apps using the gem to actually calculate the integrity digest. For the other languages (django, jijna, liquid, mustache) we have defined a stylesheet_include_tag method in their respective processors that will translate that tag into something they can understand:
<link href="<%= asset_path "govuk-template-print.css" %>" media="print" rel="stylesheet" />
  • Loading branch information
Elena Tanasoiu committed May 15, 2017
1 parent 155a2fb commit 967298a
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 47 deletions.
30 changes: 8 additions & 22 deletions build_tools/compiler/template_processor.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'erb'
require 'active_support/core_ext/hash'
require 'active_support/core_ext/array'

module Compiler
class TemplateProcessor
Expand Down Expand Up @@ -41,7 +43,7 @@ def method_missing(name, *args)
end

def stylesheet_link_tag(*sources)
options = stringify_keys(extract_options!(sources))
options = exclude_sri_fields(sources.extract_options!)
sources.uniq.map { |source|
link_options = {
"rel" => "stylesheet",
Expand All @@ -53,7 +55,7 @@ def stylesheet_link_tag(*sources)
end

def javascript_include_tag(*sources)
options = stringify_keys(extract_options!(sources))
options = exclude_sri_fields(sources.extract_options!)
sources.uniq.map { |source|
script_options = {
"src" => asset_path(source)
Expand All @@ -62,6 +64,10 @@ def javascript_include_tag(*sources)
}.join("\n")
end

def exclude_sri_fields(options)
options.stringify_keys.except("integrity", "crossorigin")
end

def content_tag(name, options = nil)
"<#{name}#{options}></#{name}>"
end
Expand All @@ -70,26 +76,6 @@ def tag(name, options)
"<#{name}#{options}/>"
end

def extract_options!(options)
if options.last.is_a?(Hash) && extractable_options?(options.last)
options.pop
else
{}
end
end

def extractable_options?(options)
options.instance_of?(Hash)
end

def stringify_keys(options)
result = {}
options.each_key do |key|
result[key.to_s] = options[key]
end
result
end

def tag_options(options)
return if options.empty?
output = "".dup
Expand Down
17 changes: 17 additions & 0 deletions docs/using-with-rails.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,20 @@ Or to add content to `<head>`, for stylesheets or similar:
```

Check out the [full list of blocks](template-blocks.md) you can use to customise the template.

## SRI

`govuk_template` >= 20.0.0 can be used together with `sprockets-rails` >= 3.0.0 in order to make use of the SRI

You can read more about SRI [here](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity).

SRI will add an `integrity` attribute on your script tags:

`<script src="https://example.com/example.css"
integrity="sha384oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8w"
crossorigin="anonymous"></script>`

The example above is generated automatically by sprockets-rails in your project if the integrity option is set to true:

`<%= stylesheet_script_tag 'example', integrity: true %>`

9 changes: 5 additions & 4 deletions source/views/layouts/govuk_template.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@
<meta charset="utf-8" />
<title><%= content_for?(:page_title) ? yield(:page_title) : "GOV.UK - The best place to find government services and information" %></title>

<!--[if gt IE 8]><!--><%= stylesheet_link_tag "govuk-template.css" %><!--<![endif]-->
<!--[if gt IE 8]><!--><%= stylesheet_link_tag "govuk-template.css", integrity: true, crossorigin: "anonymous" %><!--<![endif]-->
<!--[if IE 6]><link href="<%= asset_path "govuk-template-ie6.css" %>" media="screen" rel="stylesheet" /><![endif]-->
<!--[if IE 7]><link href="<%= asset_path "govuk-template-ie7.css" %>" media="screen" rel="stylesheet" /><![endif]-->
<!--[if IE 8]><link href="<%= asset_path "govuk-template-ie8.css" %>" media="screen" rel="stylesheet" /><![endif]-->
<%= stylesheet_link_tag "govuk-template-print.css", media: "print" %>
<%= stylesheet_link_tag "govuk-template-print.css", media: "print", integrity: true, crossorigin: "anonymous" %>

<!--[if IE 8]><link href="<%= asset_path "fonts-ie8.css" %>" media="all" rel="stylesheet" /><![endif]-->
<!--[if gte IE 9]><!--><%= stylesheet_link_tag "fonts.css", media: "all" %><!--<![endif]-->
<!--[if lt IE 9]><%= javascript_include_tag "ie.js" %><![endif]-->
<!--[if gte IE 9]><!--><%= stylesheet_link_tag "fonts.css", media: "all", integrity: true, crossorigin: "anonymous" %><!--<![endif]-->
<!--[if gte IE 9]><!--><%= stylesheet_link_tag "fonts.css", media: "all", integrity: true, crossorigin: "anonymous" %><!--<![endif]-->
<!--[if lt IE 9]><%= javascript_include_tag "ie.js", integrity: true, crossorigin: "anonymous" %><![endif]-->

<link rel="shortcut icon" href="<%= asset_path 'favicon.ico' %>" type="image/x-icon" />
<%# the colour used for mask-icon is the standard palette $black from
Expand Down
46 changes: 25 additions & 21 deletions spec/support/examples/processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
require 'set'

shared_examples_for "a processor" do
let(:html_erb_file) {"a/file.css"}
let(:html_erb_file) { "a/file.css" }
let(:processor) { described_class.new(html_erb_file) }

describe "convert rails tags into html" do
Expand All @@ -11,29 +11,41 @@
let(:js_source) { "ie.js" }

describe "#stylesheet_link_tag" do
let(:css_options) { {"media" => "print"} }
let(:css_options) { {"media" => "print"} }
let(:sri_attributes) { {"integrity" => true, "crossorigin" => "anonymous"} }

it "should parse the stylesheet tag" do
expect(processor.stylesheet_link_tag(css_source)).to eql("<link rel=\"stylesheet\" media=\"screen\" href=\"#{processor.asset_path(css_source)}\"/>")
end

context "if css file is for print" do
it "should parse the stylesheet tag for print" do
it "should parse the stylesheet tag and extra options" do
expect(processor.stylesheet_link_tag(css_source, css_options)).to eql("<link rel=\"stylesheet\" media=\"print\" href=\"#{processor.asset_path(css_source)}\"/>")
end
end

context "if sri attributes are present, it should ignore them" do
it "should parse the stylesheet tag without the integrity attribute" do
expect(processor.stylesheet_link_tag(css_source, sri_attributes)).to eql("<link rel=\"stylesheet\" media=\"screen\" href=\"#{processor.asset_path(css_source)}\"/>")
end
end
end

describe "#javascript_include_tag" do
let(:js_options) { {"charset" => "UTF-8"} }
let(:js_options) { {"charset" => "UTF-8"} }
let(:sri_attributes) { {"integrity" => true, "crossorigin" => "anonymous"} }

it "should parse the javascript tag" do
expect(processor.javascript_include_tag(js_source)).to eql("<script src=\"#{processor.asset_path(js_source)}\"></script>")
end

it "should parse the javascript tag" do
it "should parse the javascript tag and extra options" do
expect(processor.javascript_include_tag(js_source, js_options)).to eql("<script src=\"#{processor.asset_path(js_source)}\" charset=\"UTF-8\"></script>")
end

it "if sri attributes are present, it should ignore them" do
expect(processor.javascript_include_tag(js_source, sri_attributes)).to eql("<script src=\"#{processor.asset_path(js_source)}\"></script>")
end
end

describe "#content_tag" do
Expand All @@ -48,27 +60,19 @@
end
end

describe "#extract_options!" do
let(:options) { ["govuk-template.css", {"media" => "print"}]}

it "should extract the last part of the options" do
expect(processor.extract_options!(options)).to eql({"media" => "print"})
end
end

describe "#stringify_keys" do
let(:options) { {:media => "print"} }
describe "#tag_options" do
let(:options) { {"rel"=>"stylesheet", "media"=>"screen", "href"=>processor.asset_path(css_source) } }

it "should turn keys of a hash into strings" do
expect(processor.stringify_keys(options)).to eql({"media"=>"print"})
it "flattens the hash into a string of quoted html attributes" do
expect(processor.tag_options(options)).to eql(" rel=\"stylesheet\" media=\"screen\" href=\"#{processor.asset_path(css_source)}\"")
end
end

describe "#tag_options" do
let(:options) { {"rel"=>"stylesheet", "media"=>"screen", "href"=>processor.asset_path(css_source)} }
describe "#exclude_sri_fields" do
let(:options) { {"rel"=>"stylesheet", "media"=>"screen", "href"=>processor.asset_path(css_source), "integrity" => true, "crossorigin" => "anonymous" } }

it "should parse the hash" do
expect(processor.tag_options(options)).to eql(" rel=\"stylesheet\" media=\"screen\" href=\"#{processor.asset_path(css_source)}\"")
it "should remove the integrity and crossorigin keys from the hash" do
expect(processor.exclude_sri_fields(options)).to eql({"href" => processor.asset_path(css_source), "media" => "screen", "rel" => "stylesheet"})
end
end

Expand Down
6 changes: 6 additions & 0 deletions spec/support/uses_of_yield.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ def asset_path(*args)
def method_missing(name, *args)
puts "#{name} #{args.inspect}"
end

def stylesheet_link_tag(*sources)
end

def javascript_include_tag(*sources)
end
end

# return an array of unique values passed to yield in the templates
Expand Down

0 comments on commit 967298a

Please sign in to comment.