Skip to content

Commit 19094ec

Browse files
committed
Improve SRI support
Remove the static integrity download from the `pin` and related commands. This can't work because we modify the asset when vendoring in the application to add a comment about the source of the asset. Instead, we are by default delegating integrity calculation to the Rails asset pipeline. Fixes #308.
1 parent 46888e7 commit 19094ec

File tree

8 files changed

+86
-395
lines changed

8 files changed

+86
-395
lines changed

README.md

Lines changed: 27 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -138,85 +138,41 @@ Unpinning and removing "react"
138138

139139
## Subresource Integrity (SRI)
140140

141-
For enhanced security, importmap-rails automatically includes [Subresource Integrity (SRI)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes by default when pinning packages. This ensures that JavaScript files loaded from CDNs haven't been tampered with.
142-
143-
### Default behavior with integrity
144-
145-
When you pin a package, integrity hashes are automatically included:
146-
147-
```bash
148-
./bin/importmap pin lodash
149-
Pinning "lodash" to vendor/javascript/lodash.js via download from https://ga.jspm.io/npm:[email protected]/lodash.js
150-
Using integrity: sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF
151-
```
152-
153-
This generates a pin in your `config/importmap.rb` with the integrity hash:
154-
155-
```ruby
156-
pin "lodash", integrity: "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF" # @4.17.21
157-
```
158-
159-
### Opting out of integrity
160-
161-
If you need to disable integrity checking (not recommended for security reasons), you can use the `--no-integrity` flag:
162-
163-
```bash
164-
./bin/importmap pin lodash --no-integrity
165-
Pinning "lodash" to vendor/javascript/lodash.js via download from https://ga.jspm.io/npm:[email protected]/lodash.js
166-
```
167-
168-
This generates a pin without integrity:
169-
170-
```ruby
171-
pin "lodash" # @4.17.21
172-
```
173-
174-
### Adding integrity to existing pins
175-
176-
If you have existing pins without integrity hashes, you can add them using the `integrity` command:
177-
178-
```bash
179-
# Add integrity to specific packages
180-
./bin/importmap integrity lodash react
181-
182-
# Add integrity to all pinned packages
183-
./bin/importmap integrity
184-
185-
# Update your importmap.rb file with integrity hashes
186-
./bin/importmap integrity --update
187-
```
141+
For enhanced security, importmap-rails supports [Subresource Integrity (SRI)](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) hashes for packages loaded from external CDNs.
188142

189143
### Automatic integrity for local assets
190144

191-
For local assets served by the Rails asset pipeline (like those created with `pin` or `pin_all_from`), you can use `integrity: true` to automatically calculate integrity hashes from the compiled assets:
145+
Starting with importmap-rails, **`integrity: true` is the default** for all pins. This automatically calculates integrity hashes for local assets served by the Rails asset pipeline:
192146

193147
```ruby
194148
# config/importmap.rb
195149

196-
# Automatically calculate integrity from asset pipeline
197-
pin "application", integrity: true
198-
pin "admin", to: "admin.js", integrity: true
199-
200-
# Works with pin_all_from too
201-
pin_all_from "app/javascript/controllers", under: "controllers", integrity: true
202-
pin_all_from "app/javascript/lib", under: "lib", integrity: true
150+
# These all use integrity: true by default
151+
pin "application" # Auto-calculated integrity
152+
pin "admin", to: "admin.js" # Auto-calculated integrity
153+
pin_all_from "app/javascript/controllers", under: "controllers" # Auto-calculated integrity
203154

204-
# Mixed usage
205-
pin "local_module", integrity: true # Auto-calculated
206-
pin "cdn_package", integrity: "sha384-abc123..." # Pre-calculated
207-
pin "no_integrity_package" # No integrity (default)
155+
# Mixed usage - explicitly controlling integrity
156+
pin "cdn_package", integrity: "sha384-abc123..." # Pre-calculated hash
157+
pin "no_integrity_package", integrity: false # Explicitly disable integrity
158+
pin "nil_integrity_package", integrity: nil # Explicitly disable integrity
208159
```
209160

210161
This is particularly useful for:
211162
* **Local JavaScript files** managed by your Rails asset pipeline
212163
* **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
213164
* **Development workflow** where asset contents change frequently
214165

215-
The `integrity: true` option:
216-
* Uses the Rails asset pipeline's built-in integrity calculation
217-
* Works with both Sprockets and Propshaft
218-
* Automatically updates when assets are recompiled
219-
* Gracefully handles missing assets (returns `nil` for non-existent files)
166+
This behavior can be disabled by setting `integrity: false` or `integrity: nil`
167+
168+
**Important for Propshaft users:** SRI support requires Propshaft 1.2+ and you must configure the integrity hash algorithm in your application:
169+
170+
```ruby
171+
# config/application.rb or config/environments/*.rb
172+
config.assets.integrity_hash_algorithm = 'sha256' # or 'sha384', 'sha512'
173+
```
174+
175+
Without this configuration, integrity will be disabled by default when using Propshaft. Sprockets includes integrity support out of the box.
220176

221177
**Example output with `integrity: true`:**
222178
```json
@@ -240,33 +196,27 @@ The integrity hashes are automatically included in your import map and module pr
240196
```json
241197
{
242198
"imports": {
243-
"lodash": "https://ga.jspm.io/npm:[email protected]/lodash.js"
199+
"lodash": "https://ga.jspm.io/npm:[email protected]/lodash.js",
200+
"application": "/assets/application-abc123.js",
201+
"controllers/hello_controller": "/assets/controllers/hello_controller-def456.js"
244202
},
245203
"integrity": {
246204
"https://ga.jspm.io/npm:[email protected]/lodash.js": "sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF"
205+
"/assets/application-abc123.js": "sha256-xyz789...",
206+
"/assets/controllers/hello_controller-def456.js": "sha256-uvw012..."
247207
}
248208
}
249209
```
250210

251211
**Module preload tags:**
252212
```html
253213
<link rel="modulepreload" href="https://ga.jspm.io/npm:[email protected]/lodash.js" integrity="sha384-PkIkha4kVPRlGtFantHjuv+Y9mRefUHpLFQbgOYUjzy247kvi16kLR7wWnsAmqZF">
214+
<link rel="modulepreload" href="/assets/application-abc123.js" integrity="sha256-xyz789...">
215+
<link rel="modulepreload" href="/assets/controllers/hello_controller-def456.js" integrity="sha256-uvw012...">
254216
```
255217

256218
Modern browsers will automatically validate these integrity hashes when loading the JavaScript modules, ensuring the files haven't been modified.
257219

258-
### Redownloading packages with integrity
259-
260-
The `pristine` command also includes integrity by default:
261-
262-
```bash
263-
# Redownload all packages with integrity (default)
264-
./bin/importmap pristine
265-
266-
# Redownload packages without integrity
267-
./bin/importmap pristine --no-integrity
268-
```
269-
270220
## Preloading pinned modules
271221

272222
To avoid the waterfall effect where the browser has to load one file after another before it can get to the deepest nested import, importmap-rails uses [modulepreload links](https://developers.google.com/web/updates/2017/12/modulepreload) by default. If you don't want to preload a dependency, because you want to load it on-demand for efficiency, append `preload: false` to the pin.

lib/importmap/commands.rb

Lines changed: 14 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -13,51 +13,40 @@ def self.exit_on_failure?
1313
option :env, type: :string, aliases: :e, default: "production"
1414
option :from, type: :string, aliases: :f, default: "jspm"
1515
option :preload, type: :string, repeatable: true, desc: "Can be used multiple times"
16-
option :integrity, type: :boolean, aliases: :i, default: true, desc: "Include integrity hash from JSPM"
1716
def pin(*packages)
18-
with_import_response(packages, env: options[:env], from: options[:from], integrity: options[:integrity]) do |imports, integrity_hashes|
19-
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
20-
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
17+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
18+
puts %(Pinning "#{package}" to #{packager.vendor_path}/#{package}.js via download from #{url})
2119

22-
packager.download(package, url)
20+
packager.download(package, url)
2321

24-
pin = packager.vendored_pin_for(package, url, options[:preload], integrity: integrity_hash)
22+
pin = packager.vendored_pin_for(package, url, options[:preload])
2523

26-
log_integrity_usage(integrity_hash)
27-
update_importmap_with_pin(package, pin)
28-
end
24+
update_importmap_with_pin(package, pin)
2925
end
3026
end
3127

3228
desc "unpin [*PACKAGES]", "Unpin existing packages"
3329
option :env, type: :string, aliases: :e, default: "production"
3430
option :from, type: :string, aliases: :f, default: "jspm"
3531
def unpin(*packages)
36-
with_import_response(packages, env: options[:env], from: options[:from]) do |imports, _integrity_hashes|
37-
imports.each do |package, url|
38-
if packager.packaged?(package)
39-
puts %(Unpinning and removing "#{package}")
40-
packager.remove(package)
41-
end
32+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
33+
if packager.packaged?(package)
34+
puts %(Unpinning and removing "#{package}")
35+
packager.remove(package)
4236
end
4337
end
4438
end
4539

4640
desc "pristine", "Redownload all pinned packages"
4741
option :env, type: :string, aliases: :e, default: "production"
4842
option :from, type: :string, aliases: :f, default: "jspm"
49-
option :integrity, type: :boolean, aliases: :i, default: true, desc: "Include integrity hash from JSPM"
5043
def pristine
5144
packages = prepare_packages_with_versions
5245

53-
with_import_response(packages, env: options[:env], from: options[:from], integrity: options[:integrity]) do |imports, integrity_hashes|
54-
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
55-
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
46+
for_each_import(packages, env: options[:env], from: options[:from]) do |package, url|
47+
puts %(Downloading "#{package}" to #{packager.vendor_path}/#{package}.js from #{url})
5648

57-
packager.download(package, url)
58-
59-
log_integrity_usage(integrity_hash)
60-
end
49+
packager.download(package, url)
6150
end
6251
end
6352

@@ -118,33 +107,6 @@ def packages
118107
puts npm.packages_with_versions.map { |x| x.join(' ') }
119108
end
120109

121-
desc "integrity [*PACKAGES]", "Download and add integrity hashes for packages"
122-
option :env, type: :string, aliases: :e, default: "production"
123-
option :from, type: :string, aliases: :f, default: "jspm"
124-
option :update, type: :boolean, aliases: :u, default: false, desc: "Update importmap.rb with integrity hashes"
125-
def integrity(*packages)
126-
packages = prepare_packages_with_versions(packages)
127-
128-
with_import_response(packages, env: options[:env], from: options[:from], integrity: true) do |imports, integrity_hashes|
129-
process_imports(imports, integrity_hashes) do |package, url, integrity_hash|
130-
puts %(Getting integrity for "#{package}" from #{url})
131-
132-
if integrity_hash
133-
puts %( #{package}: #{integrity_hash})
134-
135-
if options[:update]
136-
pin_with_integrity = packager.pin_for(package, url, integrity: integrity_hash)
137-
138-
update_importmap_with_pin(package, pin_with_integrity)
139-
puts %( Updated importmap.rb with integrity for "#{package}")
140-
end
141-
else
142-
puts %( No integrity hash available for "#{package}")
143-
end
144-
end
145-
end
146-
end
147-
148110
private
149111
def packager
150112
@packager ||= Importmap::Packager.new
@@ -162,10 +124,6 @@ def update_importmap_with_pin(package, pin)
162124
end
163125
end
164126

165-
def log_integrity_usage(integrity_hash)
166-
puts %( Using integrity: #{integrity_hash}) if integrity_hash
167-
end
168-
169127
def handle_package_not_found(packages, from)
170128
puts "Couldn't find any packages in #{packages.inspect} on #{from}"
171129
end
@@ -205,18 +163,11 @@ def prepare_packages_with_versions(packages = [])
205163
end
206164
end
207165

208-
def process_imports(imports, integrity_hashes, &block)
209-
imports.each do |package, url|
210-
integrity_hash = integrity_hashes[url]
211-
block.call(package, url, integrity_hash)
212-
end
213-
end
214-
215-
def with_import_response(packages, **options)
166+
def for_each_import(packages, **options, &block)
216167
response = packager.import(*packages, **options)
217168

218169
if response
219-
yield response[:imports], response[:integrity]
170+
response[:imports].each(&block)
220171
else
221172
handle_package_not_found(packages, options[:from])
222173
end

lib/importmap/map.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ def draw(path = nil, &block)
2525
self
2626
end
2727

28-
def pin(name, to: nil, preload: true, integrity: nil)
28+
def pin(name, to: nil, preload: true, integrity: true)
2929
clear_cache
3030
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
3131
end
3232

33-
def pin_all_from(dir, under: nil, to: nil, preload: true, integrity: nil)
33+
def pin_all_from(dir, under: nil, to: nil, preload: true, integrity: true)
3434
clear_cache
3535
@directories[dir] = MappedDir.new(dir: dir, under: under, path: to, preload: preload, integrity: integrity)
3636
end

lib/importmap/packager.rb

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ def initialize(importmap_path = "config/importmap.rb", vendor_path: "vendor/java
1717
@vendor_path = Pathname.new(vendor_path)
1818
end
1919

20-
def import(*packages, env: "production", from: "jspm", integrity: false)
20+
def import(*packages, env: "production", from: "jspm")
2121
response = post_json({
2222
"install" => Array(packages),
2323
"flattenScope" => true,
2424
"env" => [ "browser", "module", env ],
2525
"provider" => normalize_provider(from),
26-
"integrity" => integrity
2726
})
2827

2928
case response.code
@@ -36,20 +35,19 @@ def import(*packages, env: "production", from: "jspm", integrity: false)
3635
end
3736
end
3837

39-
def pin_for(package, url = nil, preloads: nil, integrity: nil)
38+
def pin_for(package, url = nil, preloads: nil)
4039
to = url ? %(, to: "#{url}") : ""
4140
preload_param = preload(preloads)
42-
integrity_param = integrity ? %(, integrity: "#{integrity}") : ""
4341

44-
%(pin "#{package}") + to + preload_param + integrity_param
42+
%(pin "#{package}") + to + preload_param
4543
end
4644

47-
def vendored_pin_for(package, url, preloads = nil, integrity: nil)
45+
def vendored_pin_for(package, url, preloads = nil)
4846
filename = package_filename(package)
4947
version = extract_package_version_from(url)
5048
to = "#{package}.js" != filename ? filename : nil
5149

52-
pin_for(package, to, preloads: preloads, integrity: integrity) + %( # #{version})
50+
pin_for(package, to, preloads: preloads) + %( # #{version})
5351
end
5452

5553
def packaged?(package)
@@ -96,11 +94,9 @@ def normalize_provider(name)
9694
def extract_parsed_response(response)
9795
parsed = JSON.parse(response.body)
9896
imports = parsed.dig("map", "imports")
99-
integrity = parsed.dig("map", "integrity") || {}
10097

10198
{
10299
imports: imports,
103-
integrity: integrity
104100
}
105101
end
106102

0 commit comments

Comments
 (0)