Skip to content
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

Add support for subresource integrity #506

Open
denisahearn opened this issue Aug 1, 2024 · 2 comments
Open

Add support for subresource integrity #506

denisahearn opened this issue Aug 1, 2024 · 2 comments

Comments

@denisahearn
Copy link

My company has asked me to implement subresource integrity security in our Rails application. We're using shakapacker/webpack for our JavaScript assets (Sprockets is handling the bundling of our stylesheets and images). After researching how to do this, I came to learn that SRI isn't a first class feature of Shakapacker, however there is some information out there that helped me to roll my own solution.

config/webpack/webpack.config.js

const { generateWebpackConfig, merge } = require('shakapacker')
const { SubresourceIntegrityPlugin } = require('webpack-subresource-integrity')
const WebpackAssetsManifest = require('webpack-assets-manifest')

const webpackConfig = generateWebpackConfig()

const customConfig = {
  output: {
    // the following setting is required for SRI to work:
    crossOriginLoading: "anonymous",
  },
  plugins: [
    new SubresourceIntegrityPlugin(),
    new WebpackAssetsManifest(
      {
        entrypoints: true,
        integrity: true,
        writeToDisk: true,
        entrypointsUseAssets: true,
        publicPath: true,
        output: 'manifest.json'
      }
    )
  ]
}

// https://github.com/shakacode/shakapacker/discussions/85
const filteredPlugins = webpackConfig.plugins.filter((plugin) => !(plugin instanceof WebpackAssetsManifest))
webpackConfig.plugins = filteredPlugins

module.exports = merge(webpackConfig, customConfig)

app/views/layouts/application.html.erb

<head>
   ...
  <%= javascript_pack_tag 'application', defer: false, skip_pipeline: true %>
</head>

app/helpers/asset_helper.rb

# Based on https://github.com/rails/webpacker/issues/323#issuecomment-547616109
# rubocop:disable Style/ClassAndModuleChildren
module AssetHelper
  # This monkey patch is needed to set the integrity attr to html tags for SRI.
  # It was developed using information at the link above, along with lifting
  # code out of the shakapacker gem, version 8.0.0, and modifying it.
  module Shakapacker::Helper
    def javascript_pack_tag(*names, defer: true, **options)
      if @javascript_pack_tag_loaded
        raise 'To prevent duplicated chunks on the page, you should call javascript_pack_tag only once on the page. ' \
              'Please refer to https://github.com/shakacode/shakapacker/blob/main/README.md#view-helpers-javascript_pack_tag-and-stylesheet_pack_tag for the usage guide'
      end

      append_javascript_pack_tag(*names, defer: defer)
      non_deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:non_deferred], type: :javascript)
      deferred = sources_from_manifest_entrypoints(javascript_pack_tag_queue[:deferred], type: :javascript) - non_deferred

      @javascript_pack_tag_loaded = true

      capture do
        render_javascript_tags(deferred, options, true)
        concat "\n" if non_deferred.any? && deferred.any?
        render_javascript_tags(non_deferred, options, false)
      end
    end

    private def render_javascript_tags(sources, options, defer)
      last_index = sources.size - 1
      sources.each.with_index do |source, index|
        tag_source = lookup_source(source)
        tag_options = options.merge(defer: defer)
        if include_subresource_integrity?
          #:nocov:
          integrity = lookup_integrity(source)
          tag_options.merge!(integrity: integrity, crossorigin: 'anonymous') if integrity.present?
          #:nocov:
        end
        concat javascript_include_tag(tag_source, **tag_options)
        concat "\n" unless index == last_index
      end
    end

    private def lookup_source(source)
      (source.respond_to?(:dig) && source.dig('src')) || source # rubocop:disable Style/SingleArgumentDig
    end

    private def lookup_integrity(source)
      (source.respond_to?(:dig) && source.dig('integrity')) || nil # rubocop:disable Style/SingleArgumentDig
    end

    # Only serve integrity metadata for HTTPS requests:
    # http://www.w3.org/TR/SRI/#non-secure-contexts-remain-non-secure
    private def include_subresource_integrity?
      respond_to?(:request) && request && request.ssl?
    end
  end
end
# rubocop:enable Style/ClassAndModuleChildren

(Disclaimer: The view helper code above probably does not handle all scenarios. I wrote it to work with our Rails application and we still need to take it through our QA process. If anyone decides to use it, I highly suggest scrutinizing that code to make sure it is appropriate for your application.)

Desired behavior:

SRI support is a first-class feature in shakapacker, and easily enabled by doing something like this:

config/shakapacker.yml

integrity_enabled: true

view template

  <%= javascript_pack_tag 'application', defer: false, integrity: true %>

Actual behavior:

SRI support must be added through custom coding

Setup environment:

  • Ruby version: Ruby 3.3
  • Rails version: Rails 7.1
  • Shakapacker version: 8.0.0
@G-Rath
Copy link
Contributor

G-Rath commented Aug 4, 2024

hey thanks for sharing this - I know vaguely of SRI but it's not something I've actively looked into, but I think if it's something we could make painless to do via Shakapacker (ideally to the point that it could just be on by default) I think that'd be a really awesome security win.

I'll try to get sometime this month to look over your code in more detail and what it might take to get it integrated in - please feel free to post any updates here, and I'll do the same about any learnings I have :)

@denisahearn
Copy link
Author

@G-Rath Sounds good. I noticed that the Rails Sprockets gem has support for SRI, although its documentation states that the support is considered experimental at this point. It's an implementation to look at for ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants