Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/web-auth/captcha.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ var defaults = {
: 'Solve the formula shown above';
return (
'<div class="captcha-challenge">\n' +
' <img src="' +
challenge.image +
'" />\n' +
' <img src="" />\n' +
' <button type="button" class="captcha-reload">↺</button>\n' +
'</div>\n' +
'<input type="text" name="captcha"\n' +
Expand Down Expand Up @@ -71,6 +69,12 @@ var defaults = {

function handleAuth0Provider(element, options, challenge, load) {
element.innerHTML = options.templates[challenge.provider](challenge);
// Use setAttribute to safely assign challenge.image — avoids HTML injection via innerHTML string concat.
// If a custom template is used that omits .captcha-challenge img, src will not be set (by design).
var img = element.querySelector('.captcha-challenge img');
if (img) {
img.setAttribute('src', challenge.image || '');
}
element
.querySelector('.captcha-reload')
.addEventListener('click', function (e) {
Expand Down
36 changes: 36 additions & 0 deletions test/web-auth/captcha.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,18 @@ describe('captcha rendering', function () {
expect(imgEl.src).to.equal(challenges[0].image);
});

it('should not inject HTML attributes when challenge.image contains a malicious payload', function () {
const { window: w } = new JSDOM('<body><div class="captcha" style="display: none;" /></body>');
const el = w.document.querySelector('.captcha');
const maliciousChallenge = { required: true, provider: 'auth0', image: 'x" onerror="alert(1)" x="' };
const mockClient = { getChallenge: cb => cb(null, maliciousChallenge) };
captcha.render(mockClient, captcha.Flow.DEFAULT, el, {});
const imgEl = el.querySelector('img');
expect(imgEl).to.be.ok();
expect(imgEl.getAttribute('onerror')).to.equal(null);
expect(imgEl.getAttribute('src')).to.equal(maliciousChallenge.image);
});

it('should contain an input tag with name captcha', function () {
const inputEl = element.querySelector('input[name="captcha"]');
expect(inputEl).to.be.ok();
Expand Down Expand Up @@ -762,6 +774,18 @@ describe('passwordless captcha rendering', function () {
expect(imgEl.src).to.equal(challenges[0].image);
});

it('should not inject HTML attributes when challenge.image contains a malicious payload', function () {
const { window: w } = new JSDOM('<body><div class="captcha" style="display: none;" /></body>');
const el = w.document.querySelector('.captcha');
const maliciousChallenge = { required: true, provider: 'auth0', image: 'x" onerror="alert(1)" x="' };
const mockClient = { passwordless: { getChallenge: cb => cb(null, maliciousChallenge) } };
captcha.render(mockClient, captcha.Flow.PASSWORDLESS, el, {});
const imgEl = el.querySelector('img');
expect(imgEl).to.be.ok();
expect(imgEl.getAttribute('onerror')).to.equal(null);
expect(imgEl.getAttribute('src')).to.equal(maliciousChallenge.image);
});

it('should contain an input tag with name captcha', function () {
const inputEl = element.querySelector('input[name="captcha"]');
expect(inputEl).to.be.ok();
Expand Down Expand Up @@ -1378,6 +1402,18 @@ describe('password reset captcha rendering', function () {
expect(imgEl.src).to.equal(challenges[0].image);
});

it('should not inject HTML attributes when challenge.image contains a malicious payload', function () {
const { window: w } = new JSDOM('<body><div class="captcha" style="display: none;" /></body>');
const el = w.document.querySelector('.captcha');
const maliciousChallenge = { required: true, provider: 'auth0', image: 'x" onerror="alert(1)" x="' };
const mockClient = { dbConnection: { getPasswordResetChallenge: cb => cb(null, maliciousChallenge) } };
captcha.render(mockClient, captcha.Flow.PASSWORD_RESET, el, {});
const imgEl = el.querySelector('img');
expect(imgEl).to.be.ok();
expect(imgEl.getAttribute('onerror')).to.equal(null);
expect(imgEl.getAttribute('src')).to.equal(maliciousChallenge.image);
});

it('should contain an input tag with name captcha', function () {
const inputEl = element.querySelector('input[name="captcha"]');
expect(inputEl).to.be.ok();
Expand Down