Skip to content

Commit 5037a13

Browse files
fresh-eggsjhawthorn
authored andcommitted
Ignore certain data-* attributes in rails-ujs when element is contenteditable
There is a potential DOM based cross-site scripting issue in rails-ujs which leverages the Clipboard API to target HTML elements that are assigned the contenteditable attribute. This has the potential to occur when pasting malicious HTML content from the clipboard that includes a data-method, data-disable-with or data-remote attribute. [CVE-2023-23913]
1 parent 3cf23c3 commit 5037a13

File tree

7 files changed

+91
-2
lines changed

7 files changed

+91
-2
lines changed

actionview/app/assets/javascripts/rails-ujs/features/disable.coffee

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#= require_tree ../utils
22

3-
{ matches, getData, setData, stopEverything, formElements } = Rails
3+
{ matches, getData, setData, stopEverything, formElements, isContentEditable } = Rails
44

55
Rails.handleDisabledElement = (e) ->
66
element = this
@@ -14,6 +14,9 @@ Rails.enableElement = (e) ->
1414
else
1515
element = e
1616

17+
if isContentEditable(element)
18+
return
19+
1720
if matches(element, Rails.linkDisableSelector)
1821
enableLinkElement(element)
1922
else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formEnableSelector)
@@ -24,6 +27,10 @@ Rails.enableElement = (e) ->
2427
# Unified function to disable an element (link, button and form)
2528
Rails.disableElement = (e) ->
2629
element = if e instanceof Event then e.target else e
30+
31+
if isContentEditable(element)
32+
return
33+
2734
if matches(element, Rails.linkDisableSelector)
2835
disableLinkElement(element)
2936
else if matches(element, Rails.buttonDisableSelector) or matches(element, Rails.formDisableSelector)

actionview/app/assets/javascripts/rails-ujs/features/method.coffee

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#= require_tree ../utils
22

33
{ stopEverything } = Rails
4+
{ isContentEditable } = Rails
45

56
# Handles "data-method" on links such as:
67
# <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
@@ -9,6 +10,9 @@ Rails.handleMethod = (e) ->
910
method = link.getAttribute('data-method')
1011
return unless method
1112

13+
if isContentEditable(this)
14+
return
15+
1216
href = Rails.href(link)
1317
csrfToken = Rails.csrfToken()
1418
csrfParam = Rails.csrfParam()

actionview/app/assets/javascripts/rails-ujs/features/remote.coffee

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
matches, getData, setData
55
fire, stopEverything
66
ajax, isCrossDomain
7-
serializeElement
7+
serializeElement,
8+
isContentEditable
89
} = Rails
910

1011
# Checks "data-remote" if true to handle the request through a XHR request.
@@ -21,6 +22,10 @@ Rails.handleRemote = (e) ->
2122
fire(element, 'ajax:stopped')
2223
return false
2324

25+
if isContentEditable(element)
26+
fire(element, 'ajax:stopped')
27+
return false
28+
2429
withCredentials = element.getAttribute('data-with-credentials')
2530
dataType = element.getAttribute('data-type') or 'script'
2631

actionview/app/assets/javascripts/rails-ujs/utils/dom.coffee

+12
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ Rails.setData = (element, key, value) ->
2929
element[expando] ?= {}
3030
element[expando][key] = value
3131

32+
Rails.isContentEditable = (element) ->
33+
isEditable = false
34+
loop
35+
if(element.isContentEditable)
36+
isEditable = true
37+
break
38+
39+
element = element.parentElement
40+
break unless(element)
41+
42+
return isEditable
43+
3244
# a wrapper for document.querySelectorAll
3345
# returns an Array
3446
Rails.$ = (selector) ->

actionview/test/ujs/public/test/data-disable-with.js

+22
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ module('data-disable-with', {
3737
'data-url': '/echo',
3838
'data-disable-with': 'clicking...'
3939
}))
40+
41+
$('#qunit-fixture').append($('<div />', {
42+
id: 'edit-div', 'contenteditable': 'true'
43+
}))
4044
},
4145
teardown: function() {
4246
$(document).unbind('iframe:loaded')
@@ -432,3 +436,21 @@ asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` e
432436
start()
433437
}, 30)
434438
})
439+
440+
asyncTest('form button with "data-disable-with" attribute and contenteditable is not modified', 6, function() {
441+
var form = $('form[data-remote]'), button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>')
442+
443+
var contenteditable_div = $('#qunit-fixture').find('div')
444+
form.append(button)
445+
contenteditable_div.append(form)
446+
447+
App.checkEnabledState(button, 'Submit')
448+
449+
setTimeout(function() {
450+
App.checkEnabledState(button, 'Submit')
451+
start()
452+
}, 13)
453+
form.triggerNative('submit')
454+
455+
App.checkEnabledState(button, 'Submit')
456+
})

actionview/test/ujs/public/test/data-method.js

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ module('data-method', {
55
$('#qunit-fixture').append($('<a />', {
66
href: '/echo', 'data-method': 'delete', text: 'destroy!'
77
}))
8+
9+
$('#qunit-fixture').append($('<div />', {
10+
id: 'edit-div', 'contenteditable': 'true'
11+
}))
812
},
913
teardown: function() {
1014
$(document).unbind('iframe:loaded')
@@ -82,4 +86,19 @@ asyncTest('link with "data-method" and cross origin', 1, function() {
8286
notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae')
8387
})
8488

89+
asyncTest('do not interact with contenteditable elements', 6, function() {
90+
var contenteditable_div = $('#qunit-fixture').find('div')
91+
contenteditable_div.append('<a href="http://www.shouldnevershowindocument.com" data-method="delete">')
92+
93+
var link = $('#edit-div').find('a')
94+
link.triggerNative('click')
95+
96+
start()
97+
98+
collection = document.getElementsByTagName('form')
99+
for (const item of collection) {
100+
notEqual(item.action, "http://www.shouldnevershowindocument.com/")
101+
}
102+
})
103+
85104
})()

actionview/test/ujs/public/test/data-remote.js

+20
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ module('data-remote', {
4141
}))
4242
.find('form').append($('<input type="text" name="user_name" value="john">'))
4343

44+
$('#qunit-fixture').append($('<div />', {
45+
id: 'edit-div', 'contenteditable': 'true'
46+
}))
4447
}
4548
})
4649

@@ -508,4 +511,21 @@ asyncTest('inputs inside disabled fieldset are not submitted on remote forms', 3
508511
.triggerNative('submit')
509512
})
510513

514+
asyncTest('clicking on a link with contenteditable attribute does not fire ajaxyness', 0, function() {
515+
var contenteditable_div = $('#qunit-fixture').find('div')
516+
var link = $('a[data-remote]')
517+
contenteditable_div.append(link)
518+
519+
link
520+
.bindNative('ajax:beforeSend', function() {
521+
ok(false, 'ajax should not be triggered')
522+
})
523+
.bindNative('click', function(e) {
524+
e.preventDefault()
525+
})
526+
.triggerNative('click')
527+
528+
setTimeout(function() { start() }, 20)
529+
})
530+
511531
})()

0 commit comments

Comments
 (0)