Fix data string escape leakage by moving error strings to the DOM (Fixes #12789)

This commit is contained in:
Alex Gibson 2023-02-23 10:24:34 +00:00
Родитель 3d23083de6
Коммит 02f3447b06
3 изменённых файлов: 91 добавлений и 48 удалений

Просмотреть файл

@ -20,29 +20,6 @@
{% block body_class %}newsletter-management{% endblock %} {% block body_class %}newsletter-management{% endblock %}
{% set recovery_href = 'href="' + url('newsletter.recovery') + '"'|escape %}
{% block string_data %}
{#
Note the outer single quotes wrapping data-error-token-not-found, not
doubles - this is critical to avoiding some over-escaping of quotes
in `recovery_href` when we bleach during the ftl() call
See: https://github.com/mozilla/bedrock/issues/12789
#}
#}
{% if ftl_has_messages('newsletters-the-supplied-link-has-expired-v2') %}
data-error-token-not-found='{{ ftl("newsletters-the-supplied-link-has-expired-v2", recovery_href=recovery_href) }}'
{% else %}
data-error-token-not-found='{{ ftl("newsletters-the-supplied-link-has-expired") }}'
{% endif %}
data-error-invalid-email="{{ ftl('newsletters-this-is-not-a-valid-email') }}"
data-error-invalid-newsletter="{{ ftl('newsletters-is-not-a-valid-newsletter', newsletter='%newsletter%') }}"
data-error-select-country="{{ ftl('newsletters-please-select-country') }}"
data-error-select-lang="{{ ftl('newsletters-please-select-language') }}"
data-error-try-again-later="{{ ftl('newsletters-something-is-amiss-with') }}"
{% endblock %}
{% set maintenance_mode = switch('newsletter-maintenance-mode') %} {% set maintenance_mode = switch('newsletter-maintenance-mode') %}
{% block content %} {% block content %}
@ -168,6 +145,35 @@
</div> </div>
{% endif %} {% endif %}
</main> </main>
{# Error strings exist in the DOM instead of JSON as they can still be needed when fetching data fails! #}
{% set recovery_href = 'href="' + url('newsletter.recovery') + '"' %}
<div class="template-error-strings" hidden>
<ul>
<li class="error-token-not-found">
{% if ftl_has_messages('newsletters-the-supplied-link-has-expired-v2') %}
{{ ftl("newsletters-the-supplied-link-has-expired-v2", recovery_href=recovery_href) }}
{% else %}
{{ ftl("newsletters-the-supplied-link-has-expired") }}
{% endif %}
</li>
<li class="error-invalid-email">
{{ ftl('newsletters-this-is-not-a-valid-email') }}
</li>
<li class="error-invalid-newsletter">
{{ ftl('newsletters-is-not-a-valid-newsletter', newsletter='%newsletter%') }}
</li>
<li class="error-select-country">
{{ ftl('newsletters-please-select-country') }}
</li>
<li class="error-select-lang">
{{ ftl('newsletters-please-select-language') }}
</li>
<li class="error-try-again-later">
{{ ftl('newsletters-something-is-amiss-with') }}
</li>
</ul>
</div>
{% endblock %} {% endblock %}
{# Don't display the footer if there is a token present. bug 1247446 #} {# Don't display the footer if there is a token present. bug 1247446 #}

Просмотреть файл

@ -479,8 +479,7 @@ const NewsletterManagementForm = {
FormUtils.clearFormErrors(_form); FormUtils.clearFormErrors(_form);
errors.forEach((error) => { errors.forEach((error) => {
const item = `<li>${error}</li>`; list.insertAdjacentElement('afterbegin', error);
list.insertAdjacentHTML('afterbegin', item);
}); });
errorContainer.classList.remove('hidden'); errorContainer.classList.remove('hidden');
@ -538,32 +537,40 @@ const NewsletterManagementForm = {
* @returns {String} * @returns {String}
*/ */
handleFormError: (msg, newsletterId) => { handleFormError: (msg, newsletterId) => {
const strings = document.getElementById('strings'); const strings = document.querySelector('.template-error-strings');
let error; let error;
switch (msg) { switch (msg) {
case FormUtils.errorList.NOT_FOUND: case FormUtils.errorList.NOT_FOUND:
error = strings.getAttribute('data-error-token-not-found'); error = strings.querySelector('.error-token-not-found');
break; break;
case FormUtils.errorList.EMAIL_INVALID_ERROR: case FormUtils.errorList.EMAIL_INVALID_ERROR:
error = strings.getAttribute('data-error-invalid-email'); error = strings.querySelector('.error-invalid-email');
break; break;
case FormUtils.errorList.NEWSLETTER_ERROR: case FormUtils.errorList.NEWSLETTER_ERROR:
error = strings.getAttribute('data-error-invalid-newsletter'); error = strings.querySelector('.error-invalid-newsletter');
// replace '%newsletter%' placeholder with actual newsletter ID. // replace '%newsletter%' placeholder with actual newsletter ID.
if (typeof newsletterId === 'string') { if (typeof newsletterId === 'string') {
error = error.replace('%newsletter%', newsletterId); const temp = error.textContent.replace(
'%newsletter%',
newsletterId
);
error.textContent = temp;
} }
break; break;
case FormUtils.errorList.COUNTRY_ERROR: case FormUtils.errorList.COUNTRY_ERROR:
error = strings.getAttribute('data-error-select-country'); error = strings.querySelector('.error-select-country');
break; break;
case FormUtils.errorList.LANGUAGE_ERROR: case FormUtils.errorList.LANGUAGE_ERROR:
error = strings.getAttribute('data-error-select-lang'); error = strings.querySelector('.error-select-lang');
break; break;
default: default:
error = strings.getAttribute('data-error-try-again-later'); error = strings.querySelector('.error-try-again-later');
}
if (error) {
return error.cloneNode(true);
} }
return error; return error;
@ -797,7 +804,11 @@ const NewsletterManagementForm = {
NewsletterManagementForm.onDataError(e); NewsletterManagementForm.onDataError(e);
}); });
}) })
.catch(NewsletterManagementForm.redirectToRecoveryPage); .catch(() => {
if (!FormUtils.getUserToken()) {
NewsletterManagementForm.redirectToRecoveryPage();
}
});
} }
}; };

Просмотреть файл

@ -13,7 +13,6 @@ const TOKEN_MOCK = 'a1a2a3a4-abc1-12ab-a123-12345a12345b';
describe('management.es6.js', function () { describe('management.es6.js', function () {
beforeEach(function () { beforeEach(function () {
const form = `<div id="newsletter-management-test-form"> const form = `<div id="newsletter-management-test-form">
<div id="strings" data-error-token-not-found="The supplied link has expired. You will receive a new one in the next newsletter." data-error-invalid-email="This is not a valid email address. Please check the spelling." data-error-invalid-newsletter="%newsletter% is not a valid newsletter" data-error-select-country="Please select a country or region" data-error-select-lang="Please select a language" data-error-try-again-later="Something is amiss with our system, sorry! Please try again later."></div>
<header class="mzp-l-content mzp-t-content-lg"> <header class="mzp-l-content mzp-t-content-lg">
<h1>Manage your Email Preferences</h1> <h1>Manage your Email Preferences</h1>
<div class="js-intro-msg"> <div class="js-intro-msg">
@ -96,6 +95,28 @@ describe('management.es6.js', function () {
</div> </div>
</div> </div>
</form> </form>
<div class="template-error-strings" hidden="">
<ul>
<li class="error-token-not-found">
The supplied link has expired. Please <a href="/en-US/newsletter/recovery/">request a new link here</a>.
</li>
<li class="error-invalid-email">
This is not a valid email address. Please check the spelling.
</li>
<li class="error-invalid-newsletter">
%newsletter% is not a valid newsletter
</li>
<li class="error-select-country">
Please select a country or region
</li>
<li class="error-select-lang">
Please select a language
</li>
<li class="error-try-again-later">
Something is amiss with our system, sorry! Please try again later.
</li>
</ul>
</div>
</div>`; </div>`;
document.body.insertAdjacentHTML('beforeend', form); document.body.insertAdjacentHTML('beforeend', form);
@ -579,11 +600,16 @@ describe('management.es6.js', function () {
spyOn(NewsletterManagementForm, 'getFormLang').and.returnValue( spyOn(NewsletterManagementForm, 'getFormLang').and.returnValue(
undefined undefined
); );
expect(NewsletterManagementForm.validateFields()).toEqual([ const errors = NewsletterManagementForm.validateFields();
'bargain-hunters-weekly is not a valid newsletter', expect(errors[0].textContent.trim()).toEqual(
'Please select a country or region', 'bargain-hunters-weekly is not a valid newsletter'
);
expect(errors[1].textContent.trim()).toEqual(
'Please select a country or region'
);
expect(errors[2].textContent.trim()).toEqual(
'Please select a language' 'Please select a language'
]); );
}); });
}); });
}); });
@ -873,11 +899,11 @@ describe('management.es6.js', function () {
).and.returnValue(window.Promise.resolve(stringData)); ).and.returnValue(window.Promise.resolve(stringData));
return NewsletterManagementForm.init().then(() => { return NewsletterManagementForm.init().then(() => {
expect( const error = document
document.querySelector('.mzp-c-form-errors li:nth-child(1)') .querySelector('.mzp-c-form-errors li:nth-child(1)')
.innerText .innerHTML.trim();
).toEqual( expect(error).toEqual(
'The supplied link has expired. You will receive a new one in the next newsletter.' 'The supplied link has expired. Please <a href="/en-US/newsletter/recovery/">request a new link here</a>.'
); );
}); });
}); });
@ -899,10 +925,10 @@ describe('management.es6.js', function () {
).and.returnValue(window.Promise.resolve(stringData)); ).and.returnValue(window.Promise.resolve(stringData));
return NewsletterManagementForm.init().then(() => { return NewsletterManagementForm.init().then(() => {
expect( const error = document
document.querySelector('.mzp-c-form-errors li:nth-child(1)') .querySelector('.mzp-c-form-errors li:nth-child(1)')
.innerText .innerHTML.trim();
).toEqual( expect(error).toEqual(
'Something is amiss with our system, sorry! Please try again later.' 'Something is amiss with our system, sorry! Please try again later.'
); );
}); });