Skip to content

Commit

Permalink
feat(sanitization): refactored, fixed and extended sanitization angul…
Browse files Browse the repository at this point in the history
…ar-translate#993

- add new service $translateSanitization
- remove sanitization logic from interpolators, and implemented usage within $translateSanitization
- change $translate to use $translateSanitizationProvider for setting the strategy
- add support for using multiple sanitization strategies, which will be executed as a chain.
- throw an error when an unknown strategy name is specified. Failing silently would be a security issue.
- add methods for adding / removing strategies to $translateSanitizationProvider.

BREAKING CHANGE: You will get a warning message when using the default setting (not escaping the content).

You can fix (and remove) this warning by explicit set a sanitization strategy
within your config phase configuring $translateProvider. Even configuring the `null` mode will let the
warning disapper. You are highly encouraged specifing any mode except `null` because of security concerns.
  • Loading branch information
Mark Lagendijk authored and knalli committed Apr 25, 2015
1 parent af5d746 commit 12dbc57
Show file tree
Hide file tree
Showing 13 changed files with 512 additions and 120 deletions.
1 change: 1 addition & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ module.exports = function (grunt) {
'src/service/translate.js',
'src/service/default-interpolation.js',
'src/service/storage-key.js',
'src/service/sanitization.js',
'src/directive/translate.js',
'src/directive/translate-cloak.js',
'src/filter/translate.js'
Expand Down
3 changes: 2 additions & 1 deletion bower.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"angular": "~1.2.26",
"angular-translate-interpolation-messageformat": "2.6.1",
"angular-mocks": "~1.2.26",
"angular-cookies": "~1.2.26"
"angular-cookies": "~1.2.26",
"angular-sanitize": "~1.2.26"
},
"resolutions": {
"angular": "~1.2.26"
Expand Down
1 change: 1 addition & 0 deletions karma.unit.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = function (config) {
shared.injectByScope(scope, 'messageformat/messageformat.js'),
shared.injectByScope(scope, 'angular/angular.js'),
shared.injectByScope(scope, 'angular-cookies/angular-cookies.js'),
shared.injectByScope(scope, 'angular-sanitize/angular-sanitize.js'),
shared.injectByScope(scope, 'angular-mocks/angular-mocks.js'),
'src/translate.js',
'src/**/*.js',
Expand Down
52 changes: 16 additions & 36 deletions src/service/default-interpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,15 @@
* @description
* Uses angular's `$interpolate` services to interpolate strings against some values.
*
* @return {object} $translateInterpolator Interpolator service
* @return {object} $translateDefaultInterpolation Interpolator service
*/
angular.module('pascalprecht.translate').factory('$translateDefaultInterpolation', $translateDefaultInterpolation);

function $translateDefaultInterpolation ($interpolate) {
function $translateDefaultInterpolation ($interpolate, $translateSanitization) {

var $translateInterpolator = {},
$locale,
$identifier = 'default',
$sanitizeValueStrategy = null,
// map of all sanitize strategies
sanitizeValueStrategies = {
escaped: function (params) {
var result = {};
for (var key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
if (angular.isNumber(params[key])) {
result[key] = params[key];
} else {
result[key] = angular.element('<div></div>').text(params[key]).html();
}
}
}
return result;
}
};

var sanitizeParams = function (params) {
var result;
if (angular.isFunction(sanitizeValueStrategies[$sanitizeValueStrategy])) {
result = sanitizeValueStrategies[$sanitizeValueStrategy](params);
} else {
result = params;
}
return result;
};
$identifier = 'default';

/**
* @ngdoc function
Expand Down Expand Up @@ -71,8 +44,12 @@ function $translateDefaultInterpolation ($interpolate) {
return $identifier;
};

/**
* @deprecated will be removed in 3.0
* @see {@link pascalprecht.translate.$translateSanitization}
*/
$translateInterpolator.useSanitizeValueStrategy = function (value) {
$sanitizeValueStrategy = value;
$translateSanitization.useStrategy(value);
return this;
};

Expand All @@ -87,11 +64,14 @@ function $translateDefaultInterpolation ($interpolate) {
*
* @returns {string} interpolated string.
*/
$translateInterpolator.interpolate = function (string, interpolateParams) {
if ($sanitizeValueStrategy) {
interpolateParams = sanitizeParams(interpolateParams);
}
return $interpolate(string)(interpolateParams || {});
$translateInterpolator.interpolate = function (string, interpolationParams) {
interpolationParams = interpolationParams || {};
interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params');

var interpolatedText = $interpolate(string)(interpolationParams);
interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text');

return interpolatedText;
};

return $translateInterpolator;
Expand Down
64 changes: 26 additions & 38 deletions src/service/messageformat-interpolation.js
Original file line number Diff line number Diff line change
@@ -1,49 +1,34 @@
angular.module('pascalprecht.translate')

/**
* @ngdoc property
* @name pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
* @requires TRANSLATE_MF_INTERPOLATION_CACHE
*
* @description
* Uses MessageFormat.js to interpolate strings against some values.
*/
.constant('TRANSLATE_MF_INTERPOLATION_CACHE', '$translateMessageFormatInterpolation')

/**
* @ngdoc object
* @name pascalprecht.translate.$translateMessageFormatInterpolation
* @requires TRANSLATE_MF_INTERPOLATION_CACHE
* @requires pascalprecht.translate.TRANSLATE_MF_INTERPOLATION_CACHE
*
* @description
* Uses MessageFormat.js to interpolate strings against some values.
*
* @return {object} $translateInterpolator Interpolator service
* @return {object} $translateMessageFormatInterpolation Interpolator service
*/
.factory('$translateMessageFormatInterpolation', $translateMessageFormatInterpolation);

function $translateMessageFormatInterpolation($cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) {
function $translateMessageFormatInterpolation($translateSanitization, $cacheFactory, TRANSLATE_MF_INTERPOLATION_CACHE) {

var $translateInterpolator = {},
$cache = $cacheFactory.get(TRANSLATE_MF_INTERPOLATION_CACHE),
// instantiate with default locale (which is 'en')
$mf = new MessageFormat('en'),
$identifier = 'messageformat',
$sanitizeValueStrategy = null,
// map of all sanitize strategies
sanitizeValueStrategies = {
escaped: function (params) {
var result = {};
for (var key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
result[key] = angular.element('<div></div>').text(params[key]).html();
}
}
return result;
}
};

var sanitizeParams = function (params) {
var result;
if (angular.isFunction(sanitizeValueStrategies[$sanitizeValueStrategy])) {
result = sanitizeValueStrategies[$sanitizeValueStrategy](params);
} else {
result = params;
}
return result;
};
$identifier = 'messageformat';

if (!$cache) {
// create cache if it doesn't exist already
Expand Down Expand Up @@ -84,8 +69,12 @@ function $translateMessageFormatInterpolation($cacheFactory, TRANSLATE_MF_INTERP
return $identifier;
};

/**
* @deprecated will be removed in 3.0
* @see {@link pascalprecht.translate.$translateSanitization}
*/
$translateInterpolator.useSanitizeValueStrategy = function (value) {
$sanitizeValueStrategy = value;
$translateSanitization.useStrategy(value);
return this;
};

Expand All @@ -99,21 +88,20 @@ function $translateMessageFormatInterpolation($cacheFactory, TRANSLATE_MF_INTERP
*
* @returns {string} interpolated string.
*/
$translateInterpolator.interpolate = function (string, interpolateParams) {

interpolateParams = interpolateParams || {};
$translateInterpolator.interpolate = function (string, interpolationParams) {
interpolationParams = interpolationParams || {};
interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params');

if ($sanitizeValueStrategy) {
interpolateParams = sanitizeParams(interpolateParams);
}

var interpolatedText = $cache.get(string + angular.toJson(interpolateParams));
var interpolatedText = $cache.get(string + angular.toJson(interpolationParams));

// if given string wasn't interpolated yet, we do so now and never have to do it again
if (!interpolatedText) {
interpolatedText = $mf.compile(string)(interpolateParams);
$cache.put(string + angular.toJson(interpolateParams), interpolatedText);
interpolatedText = $mf.compile(string)(interpolationParams);
interpolatedText = $translateSanitization.sanitize(interpolatedText, 'text');

$cache.put(string + angular.toJson(interpolationParams), interpolatedText);
}

return interpolatedText;
};

Expand Down
178 changes: 178 additions & 0 deletions src/service/sanitization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* @ngdoc object
* @name pascalprecht.translate.$translateSanitization
*
* @description
* Sanitizes interpolation parameters and translated texts.
*
* @return {object} $translateSanitization sanitization service
*/
angular.module('pascalprecht.translate').provider('$translateSanitization', $translateSanitizationProvider);

function $translateSanitizationProvider () {

var provider = this,
$sanitize,
currentStrategy = null, // TODO change to either 'sanitize', 'escape' or ['sanitize', 'escapeParameters'] in 3.0.
hasConfiguredStrategy = false,
hasShownNoStrategyConfiguredWarning = false;

provider.strategies = {
/**
* Sanitizes HTML in the translation text using $sanitize.
*/
sanitize: function (value, mode) {
if (mode === 'text') {
value = htmlSanitizeValue(value);
}
return value;
},
/**
* Escapes HTML in the translation.
*/
escape: function (value, mode) {
if (mode === 'text') {
value = htmlEscapeValue(value);
}
return value;
},
/**
* Sanitizes HTML in the values of the interpolation parameters using $sanitize.
*/
sanitizeParameters: function (value, mode) {
if (mode === 'params') {
value = mapInterpolationParameters(value, htmlSanitizeValue);
}
return value;
},
/**
* Escapes HTML in the values of the interpolation parameters.
*/
escapeParameters: function (value, mode) {
if (mode === 'params') {
value = mapInterpolationParameters(value, htmlEscapeValue);
}
return value;
}
};
// Support legacy strategy name 'escaped' for backwards compatibility.
// TODO should be removed in 3.0
provider.strategies.escaped = provider.strategies.escapeParameters;

/**
* Adds a sanitization strategy to the list of known strategies.
* @param {string} strategyName
* @param {Function} strategyFunction
*/
provider.addStrategy = function (strategyName, strategyFunction) {
provider.strategies[strategyName] = strategyFunction;
};

/**
* Removes a sanitization strategy from the list of known strategies.
* @param {string} strategyName
*/
provider.removeStrategy = function (strategyName) {
delete provider.strategies[strategyName];
};

/**
* Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
* @param {string|Function|Array<string|Function>} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
* @returns {$translateSanitizationProvider}
*/
provider.useStrategy = function (strategy) {
hasConfiguredStrategy = true;
currentStrategy = strategy;
return this;
};

provider.$get = function ($injector, $log) {

var applyStrategies = function (value, mode, strategies) {
angular.forEach(strategies, function (strategy) {
if (angular.isFunction(strategy)) {
value = strategy(value, mode);
} else if (angular.isFunction(provider.strategies[strategy])) {
value = provider.strategies[strategy](value, mode);
} else {
throw new Error('pascalprecht.translate.$translateSanitization: Unknown sanitization strategy: \'' + strategy + '\'');
}
});
return value;
};

// TODO: should be removed in 3.0
var showNoStrategyConfiguredWarning = function () {
if (!hasConfiguredStrategy && !hasShownNoStrategyConfiguredWarning) {
$log.warn('pascalprecht.translate.$translateSanitization: No sanitization strategy has been configured. This can have serious security implications. See http://angular-translate.github.io/docs/#/guide/19_security for details.');
hasShownNoStrategyConfiguredWarning = true;
}
};

if ($injector.has('$sanitize')) {
$sanitize = $injector.get('$sanitize');
}

return {
/**
* Selects a sanitization strategy. When an array is provided the strategies will be executed in order.
* @param {string|Function|Array<string|Function>} strategy The sanitization strategy / strategies which should be used. Either a name of an existing strategy, a custom strategy function, or an array consisting of multiple names and / or custom functions.
* @returns {$translateSanitizationProvider}
*/
useStrategy: provider.useStrategy,
/**
* Sanitizes a value.
* @param {*} value The value which should be sanitized.
* @param {string} mode The current sanitization mode, either 'params' or 'text'.
* @param {string|Function|Array<string|Function>} [strategy] Optional custom strategy which should be used instead of the currently selected strategy.
* @returns {*}
*/
sanitize: function (value, mode, strategy) {
if (!currentStrategy) {
showNoStrategyConfiguredWarning();
}

if (arguments.length < 3) {
strategy = currentStrategy;
}

if (!strategy) {
return value;
}

var strategies = angular.isArray(strategy) ? strategy : [strategy];
return applyStrategies(value, mode, strategies);
}
};
};

var htmlEscapeValue = function (value) {
return angular.element('<div></div>').text(value).html();
};

var htmlSanitizeValue = function (value) {
if (!$sanitize) {
throw new Error('pascalprecht.translate.$translateSanitization: Error cannot find $sanitize service. Either include the ngSanitize module (https://docs.angularjs.org/api/ngSanitize) or use a sanitization strategy which does not depend on $sanitize, such as \'escape\'.');
}
return $sanitize(value);
};

var mapInterpolationParameters = function (value, iteratee) {
if (angular.isObject(value)) {
var result = angular.isArray(value) ? [] : {};

angular.forEach(value, function (propertyValue, propertyKey) {
result[propertyKey] = mapInterpolationParameters(propertyValue, iteratee);
});

return result;
}
else if (angular.isNumber(value)) {
return value;
}
else {
return iteratee(value);
}
};
}
Loading

0 comments on commit 12dbc57

Please sign in to comment.