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

- Added $translateSanitization
- Removed sanitization logic from interpolators, and implemented usage of  $translateSanitization
- Changed $translate to use $translateSanitizationProvider for setting the strategy
- Added 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.
- Added methods for adding / removing strategies to $translateSanitizationProvider.
  • Loading branch information
Mark Lagendijk committed Apr 23, 2015
1 parent 3facb92 commit 7fe5c80
Show file tree
Hide file tree
Showing 11 changed files with 399 additions and 87 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
49 changes: 14 additions & 35 deletions src/service/default-interpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,11 @@
*/
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,11 @@ function $translateDefaultInterpolation ($interpolate) {
return $identifier;
};

/**
* @deprecated ToDo: remove in 3.0
*/
$translateInterpolator.useSanitizeValueStrategy = function (value) {
$sanitizeValueStrategy = value;
$translateSanitization.useStrategy(value);
return this;
};

Expand All @@ -87,11 +63,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
51 changes: 15 additions & 36 deletions src/service/messageformat-interpolation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,13 @@ angular.module('pascalprecht.translate')
*/
.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 +61,11 @@ function $translateMessageFormatInterpolation($cacheFactory, TRANSLATE_MF_INTERP
return $identifier;
};

/**
* @deprecated ToDo: remove in 3.0
*/
$translateInterpolator.useSanitizeValueStrategy = function (value) {
$sanitizeValueStrategy = value;
$translateSanitization.useStrategy(value);
return this;
};

Expand All @@ -99,21 +79,20 @@ function $translateMessageFormatInterpolation($cacheFactory, TRANSLATE_MF_INTERP
*
* @returns {string} interpolated string.
*/
$translateInterpolator.interpolate = function (string, interpolateParams) {
$translateInterpolator.interpolate = function (string, interpolationParams) {
interpolationParams = interpolationParams || {};
interpolationParams = $translateSanitization.sanitize(interpolationParams, 'params');

interpolateParams = interpolateParams || {};

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
172 changes: 172 additions & 0 deletions src/service/sanitization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* @ngdoc object
* @name pascalprecht.translate.$translateSanitization
*
* @description
* Sanitizes interpolation parameters and translated texts.
*
* @return {object} $translateInterpolator Interpolator 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 compatability. 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(!strategy && !currentStrategy){
showNoStrategyConfiguredWarning();
return value;
}

strategy = strategy || currentStrategy;
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 7fe5c80

Please sign in to comment.