mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 15:13:21 +00:00
Merge pull request #15385 from strapi/fix/email-templates-interpolation
This commit is contained in:
commit
921d30961d
@ -1,6 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const {
|
||||
template: { createStrictInterpolationRegExp },
|
||||
keysDeep,
|
||||
} = require('@strapi/utils/');
|
||||
|
||||
const getProviderSettings = () => {
|
||||
return strapi.config.get('plugin.email');
|
||||
@ -26,10 +30,19 @@ const sendTemplatedEmail = (emailOptions = {}, emailTemplate = {}, data = {}) =>
|
||||
);
|
||||
}
|
||||
|
||||
const allowedInterpolationVariables = keysDeep(data);
|
||||
const interpolate = createStrictInterpolationRegExp(allowedInterpolationVariables, 'g');
|
||||
|
||||
const templatedAttributes = attributes.reduce(
|
||||
(compiled, attribute) =>
|
||||
emailTemplate[attribute]
|
||||
? Object.assign(compiled, { [attribute]: _.template(emailTemplate[attribute])(data) })
|
||||
? Object.assign(compiled, {
|
||||
[attribute]: _.template(emailTemplate[attribute], {
|
||||
interpolate,
|
||||
evaluate: false,
|
||||
escape: false,
|
||||
})(data),
|
||||
})
|
||||
: compiled,
|
||||
{}
|
||||
);
|
||||
|
||||
@ -24,7 +24,7 @@ const {
|
||||
joinBy,
|
||||
toKebabCase,
|
||||
} = require('./string-formatting');
|
||||
const { removeUndefined } = require('./object-formatting');
|
||||
const { removeUndefined, keysDeep } = require('./object-formatting');
|
||||
const { getConfigUrls, getAbsoluteAdminUrl, getAbsoluteServerUrl } = require('./config');
|
||||
const { generateTimestampCode } = require('./code-generator');
|
||||
const contentTypes = require('./content-types');
|
||||
@ -40,6 +40,7 @@ const traverseEntity = require('./traverse-entity');
|
||||
const pipeAsync = require('./pipe-async');
|
||||
const convertQueryParams = require('./convert-query-params');
|
||||
const importDefault = require('./import-default');
|
||||
const template = require('./template');
|
||||
|
||||
module.exports = {
|
||||
yup,
|
||||
@ -61,11 +62,13 @@ module.exports = {
|
||||
getConfigUrls,
|
||||
escapeQuery,
|
||||
removeUndefined,
|
||||
keysDeep,
|
||||
getAbsoluteAdminUrl,
|
||||
getAbsoluteServerUrl,
|
||||
generateTimestampCode,
|
||||
stringIncludes,
|
||||
stringEquals,
|
||||
template,
|
||||
isKebabCase,
|
||||
isCamelCase,
|
||||
toKebabCase,
|
||||
|
||||
@ -4,6 +4,12 @@ const _ = require('lodash');
|
||||
|
||||
const removeUndefined = (obj) => _.pickBy(obj, (value) => typeof value !== 'undefined');
|
||||
|
||||
const keysDeep = (obj, path = []) =>
|
||||
!_.isObject(obj)
|
||||
? path.join('.')
|
||||
: _.reduce(obj, (acc, next, key) => _.concat(acc, keysDeep(next, [...path, key])), []);
|
||||
|
||||
module.exports = {
|
||||
removeUndefined,
|
||||
keysDeep,
|
||||
};
|
||||
|
||||
28
packages/core/utils/lib/template.js
Normal file
28
packages/core/utils/lib/template.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Create a strict interpolation RegExp based on the given variables' name
|
||||
*
|
||||
* @param {string[]} allowedVariableNames - The list of allowed variables
|
||||
* @param {string} [flags] - The RegExp flags
|
||||
*/
|
||||
const createStrictInterpolationRegExp = (allowedVariableNames, flags) => {
|
||||
const oneOfVariables = allowedVariableNames.join('|');
|
||||
|
||||
// 1. We need to match the delimiters: <%= ... %>
|
||||
// 2. We accept any number of whitespaces characters before and/or after the variable name: \s* ... \s*
|
||||
// 3. We only accept values from the variable list as interpolation variables' name: : (${oneOfVariables})
|
||||
return new RegExp(`<%=\\s*(${oneOfVariables})\\s*%>`, flags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a loose interpolation RegExp to match as many groups as possible
|
||||
*
|
||||
* @param {string} [flags] - The RegExp flags
|
||||
*/
|
||||
const createLooseInterpolationRegExp = (flags) => new RegExp(/<%=([\s\S]+?)%>/, flags);
|
||||
|
||||
module.exports = {
|
||||
createStrictInterpolationRegExp,
|
||||
createLooseInterpolationRegExp,
|
||||
};
|
||||
@ -17,6 +17,11 @@ describe('isValidEmailTemplate', () => {
|
||||
expect(isValidEmailTemplate('<%CODE%>')).toBe(false);
|
||||
expect(isValidEmailTemplate('${CODE}')).toBe(false);
|
||||
expect(isValidEmailTemplate('${ CODE }')).toBe(false);
|
||||
expect(
|
||||
isValidEmailTemplate(
|
||||
'<%=`${ console.log({ "remote-execution": { "foo": "bar" }/*<>%=*/ }) }`%>'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('Fails on non authorized keys', () => {
|
||||
|
||||
@ -1,8 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const { trim } = require('lodash/fp');
|
||||
const {
|
||||
template: { createLooseInterpolationRegExp, createStrictInterpolationRegExp },
|
||||
} = require('@strapi/utils');
|
||||
|
||||
const invalidPatternsRegexes = [
|
||||
// Ignore "evaluation" patterns: <% ... %>
|
||||
/<%[^=]([\s\S]*?)%>/m,
|
||||
// Ignore basic string interpolations
|
||||
/\${([^{}]*)}/m,
|
||||
];
|
||||
|
||||
const invalidPatternsRegexes = [/<%[^=]([^<>%]*)%>/m, /\${([^{}]*)}/m];
|
||||
const authorizedKeys = [
|
||||
'URL',
|
||||
'ADMIN_URL',
|
||||
@ -19,27 +28,42 @@ const matchAll = (pattern, src) => {
|
||||
let match;
|
||||
|
||||
const regexPatternWithGlobal = RegExp(pattern, 'g');
|
||||
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
while ((match = regexPatternWithGlobal.exec(src))) {
|
||||
const [, group] = match;
|
||||
|
||||
matches.push(_.trim(group));
|
||||
matches.push(trim(group));
|
||||
}
|
||||
|
||||
return matches;
|
||||
};
|
||||
|
||||
const isValidEmailTemplate = (template) => {
|
||||
// Check for known invalid patterns
|
||||
for (const reg of invalidPatternsRegexes) {
|
||||
if (reg.test(template)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const matches = matchAll(/<%=([^<>%=]*)%>/, template);
|
||||
for (const match of matches) {
|
||||
if (!authorizedKeys.includes(match)) {
|
||||
return false;
|
||||
}
|
||||
const interpolation = {
|
||||
// Strict interpolation pattern to match only valid groups
|
||||
strict: createStrictInterpolationRegExp(authorizedKeys),
|
||||
// Weak interpolation pattern to match as many group as possible.
|
||||
loose: createLooseInterpolationRegExp(),
|
||||
};
|
||||
|
||||
// Compute both strict & loose matches
|
||||
const strictMatches = matchAll(interpolation.strict, template);
|
||||
const looseMatches = matchAll(interpolation.loose, template);
|
||||
|
||||
// If we have more matches with the loose RegExp than with the strict one,
|
||||
// then it means that at least one of the interpolation group is invalid
|
||||
// Note: In the future, if we wanted to give more details for error formatting
|
||||
// purposes, we could return the difference between the two arrays
|
||||
if (looseMatches.length > strictMatches.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@ -109,17 +109,25 @@ module.exports = ({ strapi }) => ({
|
||||
await this.edit(user.id, { confirmationToken });
|
||||
|
||||
const apiPrefix = strapi.config.get('api.rest.prefix');
|
||||
settings.message = await userPermissionService.template(settings.message, {
|
||||
URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
|
||||
SERVER_URL: getAbsoluteServerUrl(strapi.config),
|
||||
ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
|
||||
USER: sanitizedUserInfo,
|
||||
CODE: confirmationToken,
|
||||
});
|
||||
|
||||
settings.object = await userPermissionService.template(settings.object, {
|
||||
USER: sanitizedUserInfo,
|
||||
});
|
||||
try {
|
||||
settings.message = await userPermissionService.template(settings.message, {
|
||||
URL: urlJoin(getAbsoluteServerUrl(strapi.config), apiPrefix, '/auth/email-confirmation'),
|
||||
SERVER_URL: getAbsoluteServerUrl(strapi.config),
|
||||
ADMIN_URL: getAbsoluteAdminUrl(strapi.config),
|
||||
USER: sanitizedUserInfo,
|
||||
CODE: confirmationToken,
|
||||
});
|
||||
|
||||
settings.object = await userPermissionService.template(settings.object, {
|
||||
USER: sanitizedUserInfo,
|
||||
});
|
||||
} catch {
|
||||
strapi.log.error(
|
||||
'[plugin::users-permissions.sendConfirmationEmail]: Failed to generate a template for "user confirmation email". Please make sure your email template is valid and does not contain invalid characters or patterns'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send an email to the user.
|
||||
await strapi
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
const _ = require('lodash');
|
||||
const { filter, map, pipe, prop } = require('lodash/fp');
|
||||
const urlJoin = require('url-join');
|
||||
const {
|
||||
template: { createStrictInterpolationRegExp },
|
||||
errors,
|
||||
keysDeep,
|
||||
} = require('@strapi/utils');
|
||||
|
||||
const { getService } = require('../utils');
|
||||
|
||||
@ -230,7 +235,15 @@ module.exports = ({ strapi }) => ({
|
||||
},
|
||||
|
||||
template(layout, data) {
|
||||
const compiledObject = _.template(layout);
|
||||
return compiledObject(data);
|
||||
const allowedTemplateVariables = keysDeep(data);
|
||||
|
||||
// Create a strict interpolation RegExp based on possible variable names
|
||||
const interpolate = createStrictInterpolationRegExp(allowedTemplateVariables, 'g');
|
||||
|
||||
try {
|
||||
return _.template(layout, { interpolate, evaluate: false, escape: false })(data);
|
||||
} catch (e) {
|
||||
throw new errors.ApplicationError('Invalid email template');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user