canvas_link_migrator 1.0.19 → 1.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1295e9d9f0470130b2967896d1356ef0669d95082acf1f591347ee9896cae968
4
- data.tar.gz: c28022e251c0755942ccdadb7462335ce3d97f29594d7ef7480c847a3c40bc2a
3
+ metadata.gz: ca1247f98a0eee8de26f1fa32da2ec683f374ea995a695bb4af201d87644795a
4
+ data.tar.gz: 122790ae2f969eee61c75118096d2e9b736fc328e9a27846f8b5b2e82ec7830b
5
5
  SHA512:
6
- metadata.gz: da9757bd142342be4507fac11f7ad2c67f3b138f89a1db2e276ef5f80f363438b35512aef904ea0d6bfd9238c0d58ef90b5a21aaea0c8ededfe60456969e5bdc
7
- data.tar.gz: cb128cfa720324395074336a325587d33940d6c664bf52df33988553347a8b4907ce1434e964194902920f7564276b1b9a0d88ed38c7d403a4c419869cdbc006
6
+ metadata.gz: 37e6e2273e60f1e4428793295e8610e21c138ae16527d54d3a8885c9b5ed88e3660e521e63386ea15f7b7fdab8201f29b0732360e26dce640020c40ec9e88985
7
+ data.tar.gz: 1d7e969234f632637138adcffa681a30a2a0d5fb3391ef080010c2252fbcd2520ed9e726ab4267793d8c1402baa45b5b910a16cce74e421be740899daccc4b7c
@@ -19,16 +19,38 @@
19
19
 
20
20
  module CanvasLinkMigrator
21
21
  class LinkReplacer
22
+ HTML_ESCAPES = { '"' => "&quot;", "<" => "&lt;", ">" => "&gt;" }.freeze
23
+ HTML_ESCAPE_RE = Regexp.union(HTML_ESCAPES.keys).freeze
24
+
25
+ def self.escape_url_for_html(value)
26
+ value.to_s.gsub(HTML_ESCAPE_RE, HTML_ESCAPES)
27
+ end
28
+
22
29
  # returns false if no substitutions were made
23
30
  def self.sub_placeholders!(html, links)
24
31
  subbed = false
25
- links.each do |link|
32
+ media_links, other_links = links.partition { |l| l[:link_type] == :media_object }
33
+
34
+ if media_links.any?
35
+ by_placeholder = media_links.to_h { |l| [l[:placeholder], l] }
36
+ if html.gsub!(Regexp.union(by_placeholder.keys)) { |m|
37
+ link = by_placeholder[m]
38
+ link[:replaced] = true
39
+ link[:new_value] || link[:old_value]
40
+ }
41
+ subbed = true
42
+ end
43
+ end
44
+
45
+ other_links.each do |link|
26
46
  new_value = link[:new_value] || link[:old_value]
27
- if html.gsub!(link[:placeholder], new_value)
47
+ new_value = escape_url_for_html(new_value)
48
+ if html.gsub!(link[:placeholder]) { new_value }
28
49
  link[:replaced] = true
29
50
  subbed = true
30
51
  end
31
52
  end
53
+
32
54
  subbed
33
55
  end
34
56
 
@@ -222,7 +222,7 @@ module CanvasLinkMigrator
222
222
  case k
223
223
  when /canvas_qs_(.*)/
224
224
  qs << "#{Rack::Utils.escape($1)}=#{Rack::Utils.escape(v)}"
225
- when /canvas_(.*)/
225
+ when /\Acanvas_(\w*)\z/
226
226
  new_action += "/#{$1}"
227
227
  end
228
228
  end
@@ -1,3 +1,3 @@
1
1
  module CanvasLinkMigrator
2
- VERSION = "1.0.19"
2
+ VERSION = "1.0.20"
3
3
  end
@@ -140,6 +140,35 @@ describe CanvasLinkMigrator::ImportedHtmlConverter do
140
140
  test_string = %(<img src="%24IMS_CC_FILEBASE%24/test.png?notarelevantparam" alt="nope" />)
141
141
  expect(@converter.convert_exported_html(test_string)).to eq([%(<img src="#{@path}files/5/preview?verifier=u5" alt="nope">), nil])
142
142
  end
143
+
144
+ describe "path-segment injection via canvas_* parameter names" do
145
+ it "does not allow `..` path traversal via canvas_* keys" do
146
+ test_string = %(<img src="%24IMS_CC_FILEBASE%24/test.png?canvas_..%2F..%2Faccounts%2F1%2Fadmin=x" alt="nope" />)
147
+ html, _ = @converter.convert_exported_html(test_string)
148
+ src = Nokogiri::HTML5.fragment(html).at_css("img")["src"]
149
+ expect(src).not_to include(".."),
150
+ "path traversal: src contains `..` segments -- output was: #{src.inspect}"
151
+ end
152
+
153
+ it "does not allow a single `..` segment via canvas_* keys" do
154
+ test_string = %(<a href="%24IMS_CC_FILEBASE%24/test.png?canvas_..=x">x</a>)
155
+ html, _ = @converter.convert_exported_html(test_string)
156
+ href = Nokogiri::HTML5.fragment(html).at_css("a")["href"]
157
+ expect(href).not_to match(%r{/\.\.(?:/|\?|#|\z)}),
158
+ "path traversal: href ends in a `..` segment -- output was: #{href.inspect}"
159
+ end
160
+
161
+ it "does not allow `#` fragment injection that hides the verifier" do
162
+ test_string = %(<a href="%24IMS_CC_FILEBASE%24/test.png?canvas_a%23evil=x">x</a>)
163
+ html, _ = @converter.convert_exported_html(test_string)
164
+ href = Nokogiri::HTML5.fragment(html).at_css("a")["href"]
165
+ parsed = URI.parse(href)
166
+ expect(parsed.fragment).to be_nil,
167
+ "fragment injection: produced fragment #{parsed.fragment.inspect} -- output was: #{href.inspect}"
168
+ expect(parsed.query.to_s).to include("verifier="),
169
+ "verifier was pushed into the fragment by `#` injection -- output was: #{href.inspect}"
170
+ end
171
+ end
143
172
  end
144
173
 
145
174
  it "converts picture source srcsets" do
@@ -190,6 +219,113 @@ describe CanvasLinkMigrator::ImportedHtmlConverter do
190
219
  expect(@converter.convert_exported_html(test_string)).to eq([%(<a href="/courses/123">Mine</a><br><a href="/courses/456">Vain</a><br><a href="http://other-canvas.example.com/">Other Instance</a>), nil])
191
220
  end
192
221
 
222
+ describe "percent-encoded HTML in unresolved file references (CSAN-045)" do
223
+ it "does not allow attribute breakout via $IMS-CC-FILEBASE$ references" do
224
+ test_string = %(<img src="$IMS-CC-FILEBASE$/foo%22%09onerror=%22alert(1).jpg">)
225
+ html, _bad_links = @converter.convert_exported_html(test_string)
226
+
227
+ img = Nokogiri::HTML5.fragment(html).at_css("img")
228
+ expect(img).not_to be_nil
229
+ expect(img["onerror"]).to be_nil,
230
+ "attribute breakout: produced live onerror -- output was: #{html.inspect}"
231
+ expect(img.attributes.keys).to contain_exactly("src"),
232
+ "expected only `src`, got #{img.attributes.keys.inspect} -- output was: #{html.inspect}"
233
+ end
234
+
235
+ it "does not allow attribute breakout via plain relative URLs" do
236
+ test_string = %(<a href="relative/foo%22%09onmouseover=%22alert(1).html">x</a>)
237
+ html, _bad_links = @converter.convert_exported_html(test_string)
238
+
239
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
240
+ expect(anchor).not_to be_nil
241
+ expect(anchor["onmouseover"]).to be_nil,
242
+ "attribute breakout: produced live onmouseover -- output was: #{html.inspect}"
243
+ expect(anchor.attributes.keys).to contain_exactly("href"),
244
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
245
+ end
246
+
247
+ it "does not allow attribute breakout via $CANVAS_COURSE_REFERENCE$/file_ref/ rest" do
248
+ test_string = %(<a href="$CANVAS_COURSE_REFERENCE$/file_ref/E/preview&quot; onmouseover=&quot;alert(1)">x</a>)
249
+ html, _bad_links = @converter.convert_exported_html(test_string)
250
+
251
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
252
+ expect(anchor).not_to be_nil
253
+ expect(anchor["onmouseover"]).to be_nil,
254
+ "attribute breakout via file_ref rest -- output was: #{html.inspect}"
255
+ expect(anchor.attributes.keys).to contain_exactly("href"),
256
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
257
+ end
258
+
259
+ it "does not allow attribute breakout via $CANVAS_COURSE_REFERENCE$/modules/items/ query" do
260
+ test_string = %(<a href="$CANVAS_COURSE_REFERENCE$/modules/items/C?foo=bar&quot; onmouseover=&quot;alert(1)">x</a>)
261
+ html, _bad_links = @converter.convert_exported_html(test_string)
262
+
263
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
264
+ expect(anchor).not_to be_nil
265
+ expect(anchor["onmouseover"]).to be_nil,
266
+ "attribute breakout via module_item query -- output was: #{html.inspect}"
267
+ expect(anchor.attributes.keys).to contain_exactly("href"),
268
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
269
+ end
270
+
271
+ it "does not allow attribute breakout via wiki_page_migration_id fallback" do
272
+ test_string = %(<a href="wiki_page_migration_id=nope&quot; onmouseover=&quot;alert(1)">x</a>)
273
+ html, _bad_links = @converter.convert_exported_html(test_string)
274
+
275
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
276
+ expect(anchor).not_to be_nil
277
+ expect(anchor["onmouseover"]).to be_nil,
278
+ "attribute breakout via wiki_page fallback -- output was: #{html.inspect}"
279
+ expect(anchor.attributes.keys).to contain_exactly("href"),
280
+ "expected only `href`, got #{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
281
+ end
282
+
283
+ it "does not allow text-node injection when placeholder lives in inner_html" do
284
+ payload = "relative/foo%3Cimg%09src=x%09onerror=alert(1)%3E.html"
285
+ test_string = %(<a href="#{payload}">#{payload}</a>)
286
+ html, _bad_links = @converter.convert_exported_html(test_string)
287
+
288
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
289
+ expect(anchor).not_to be_nil
290
+ expect(anchor.css("img")).to be_empty,
291
+ "text-node injection: produced live <img> child -- output was: #{html.inspect}"
292
+ end
293
+
294
+ it "does not allow attribute injection via gsub! pre-match back-reference" do
295
+ test_string = %q(<img src="wiki_page_migration_id=NONEXIST\`onerror=alert(1) ">)
296
+ html, _bad_links = @converter.convert_exported_html(test_string)
297
+
298
+ img = Nokogiri::HTML5.fragment(html).at_css("img")
299
+ expect(img).not_to be_nil
300
+ expect(img["onerror"]).to be_nil,
301
+ "attribute injection via gsub backref: produced live onerror -- output was: #{html.inspect}"
302
+ expect(img.attributes.keys).to contain_exactly("src"),
303
+ "expected only `src`, got #{img.attributes.keys.inspect} -- output was: #{html.inspect}"
304
+ end
305
+
306
+ it "does not allow media_object cross-substitution into attribute context" do
307
+ attacker_video = %q(<video data-media-id="m-stuff" data-media-type="video"><source src="$IMS-CC-FILEBASE$/test.png" data-media-id="m-stuff" data-media-type="video"></video>)
308
+ preview = Nokogiri::HTML5.fragment(attacker_video, max_tree_depth: 10_000)
309
+ preview.search("source[data-media-type],source[data-media-id]").each do |source|
310
+ next unless %w[audio video].include?(source.parent.name)
311
+ media_node = source.parent
312
+ media_node.name = "iframe"
313
+ media_node["src"] = source["src"]
314
+ source.remove
315
+ end
316
+ predicted_placeholder = "LINK.PLACEHOLDER_#{Digest::MD5.hexdigest(preview.at_css("iframe").to_xml)}"
317
+
318
+ attack_html = %Q(<a href="evil/#{predicted_placeholder}">link</a>#{attacker_video})
319
+ html, _ = @converter.convert_exported_html(attack_html)
320
+
321
+ anchor = Nokogiri::HTML5.fragment(html).at_css("a")
322
+ expect(anchor).not_to be_nil
323
+ expect(anchor.attributes.keys).to contain_exactly("href"),
324
+ "media_object cross-substitution injected attributes onto anchor: " \
325
+ "#{anchor.attributes.keys.inspect} -- output was: #{html.inspect}"
326
+ end
327
+ end
328
+
193
329
  it "prepends course files for unrecognized relative urls" do
194
330
  test_string = %(<a href="/relative/path/to/file">Linkage</a>)
195
331
  html, bad_links = @converter.convert_exported_html(test_string)
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canvas_link_migrator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.19
4
+ version: 1.0.20
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mysti Lilla
8
8
  - James Logan
9
9
  - Sarah Gerard
10
10
  - Math Costa
11
- autorequire:
11
+ autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2026-03-24 00:00:00.000000000 Z
14
+ date: 2026-05-15 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: activesupport
@@ -139,7 +139,7 @@ dependencies:
139
139
  - - ">="
140
140
  - !ruby/object:Gem::Version
141
141
  version: '0'
142
- description:
142
+ description:
143
143
  email:
144
144
  - mysti@instructure.com
145
145
  - james.logan@instructure.com
@@ -164,11 +164,11 @@ files:
164
164
  - spec/fixtures/canvas_resource_map_pages.json
165
165
  - spec/spec_helper.rb
166
166
  - test.sh
167
- homepage:
167
+ homepage:
168
168
  licenses: []
169
169
  metadata:
170
170
  source_code_uri: https://github.com/instructure/canvas_link_migrator
171
- post_install_message:
171
+ post_install_message:
172
172
  rdoc_options: []
173
173
  require_paths:
174
174
  - lib
@@ -176,15 +176,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
176
176
  requirements:
177
177
  - - ">="
178
178
  - !ruby/object:Gem::Version
179
- version: '2.7'
179
+ version: '3.1'
180
180
  required_rubygems_version: !ruby/object:Gem::Requirement
181
181
  requirements:
182
182
  - - ">="
183
183
  - !ruby/object:Gem::Version
184
184
  version: '0'
185
185
  requirements: []
186
- rubygems_version: 3.1.6
187
- signing_key:
186
+ rubygems_version: 3.3.27
187
+ signing_key:
188
188
  specification_version: 4
189
189
  summary: Instructure gem for migrating Canvas style rich content
190
190
  test_files: []