feat(build): sanitize HTML translations in l10n
Scan all HTML translations for XSS vectors.
This commit is contained in:
Родитель
b14b59f00b
Коммит
8436e58f23
|
@ -0,0 +1,41 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var path = require('path');
|
||||
var stripSpecialChars = require('./lib/strip-special-chars');
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
grunt.registerMultiTask('generate-valid-styles', 'Generate the list of valid styles for translation', function () {
|
||||
var validStylesRegex = /style=('|")[\s\S]*?('|")/ig;
|
||||
var validStylesArray = [];
|
||||
|
||||
// iterate through .pot file and extract the styles
|
||||
this.files[0].src.forEach(function (src) {
|
||||
var contents = grunt.file.read(src);
|
||||
if (validStylesRegex.test(contents)) {
|
||||
validStylesArray = validStylesArray.concat(contents.match(validStylesRegex));
|
||||
}
|
||||
});
|
||||
validStylesArray = validStylesArray.map(stripSpecialChars);
|
||||
// write out the styles to a temporary JSON file
|
||||
grunt.file.write(path.join(this.files[0].dest, grunt.config.process('<%= yeoman.validStylesFile %>')), JSON.stringify(validStylesArray));
|
||||
grunt.log.writeln('Generated styles from ' + this.files[0].src.length + ' files');
|
||||
});
|
||||
|
||||
grunt.config('generate-valid-styles', {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
expand: false,
|
||||
src: [
|
||||
'<%= yeoman.strings_src %>/templates/**/*.pot'
|
||||
],
|
||||
dest: '<%= yeoman.tmp %>'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var path = require('path');
|
||||
|
||||
// The xss module does not run on json, it needs to form a dom tree with the html tags internally so,
|
||||
// we generate those intermediate files.
|
||||
// Four steps are performed to lint the files:
|
||||
// 1. Clean the directories
|
||||
// 2. Generate the JSON files from the PO files
|
||||
// 3. The JSON files are then written out to temporary HTML files
|
||||
// 4. The HTML files are linted
|
||||
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
grunt.registerMultiTask('l10n-json-to-html', 'Convert l10n JSON files to HTML', function () {
|
||||
var files = 0;
|
||||
this.files.forEach(function (file) {
|
||||
var HTMLcontent = '';
|
||||
var src = file.src[0];
|
||||
var pathname = src.split(path.sep);
|
||||
var localeStrings = grunt.file.readJSON(src);
|
||||
for (var val in localeStrings) {
|
||||
var value = localeStrings[val];
|
||||
if (typeof value !== 'object' && value !== '' && isNaN(value)) {
|
||||
HTMLcontent += value.toString() + '\n\n';
|
||||
}
|
||||
}
|
||||
if (HTMLcontent !== '') {
|
||||
grunt.file.write(path.join(grunt.config.process('<%= yeoman.tmp %>'),
|
||||
grunt.config.process('<%= yeoman.html %>'),
|
||||
pathname[1],
|
||||
pathname[2],
|
||||
pathname[3].replace('json', 'html')
|
||||
),
|
||||
HTMLcontent);
|
||||
files = files + 1;
|
||||
}
|
||||
});
|
||||
grunt.log.ok('%d %s converted from JSON to HTML', files, grunt.util.pluralize(files, 'file/files'));
|
||||
});
|
||||
|
||||
grunt.config('l10n-json-to-html', {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
src: [
|
||||
'<%= yeoman.tmp %>/l10n/**/*.json'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
function stripSpecialChars(str) {
|
||||
'use strict';
|
||||
// normalize the strings to use double quotes, and
|
||||
// replace newlines so that styles aren't found on
|
||||
// multiple lines
|
||||
if (typeof str === 'string' && str.length > 0) {
|
||||
str = str.replace(/'/g, '"');
|
||||
str = str.replace(/""/g, '');
|
||||
str = str.replace(/\n/g, '');
|
||||
return str.replace(/(\\)/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = stripSpecialChars;
|
|
@ -61,12 +61,12 @@ module.exports = function (grunt) {
|
|||
}
|
||||
},
|
||||
all: {
|
||||
src: ['locale/**/**/*.po'],
|
||||
dest: '.tmp/l10n/'
|
||||
src: ['<%= yeoman.strings_src %>/**/**/*.po'],
|
||||
dest: '<%= yeoman.tmp %>/l10n/'
|
||||
},
|
||||
template: {
|
||||
src: ['locale/templates/**/*.pot'],
|
||||
dest: '.tmp/l10n'
|
||||
src: ['<%= yeoman.strings_src %>/templates/**/*.pot'],
|
||||
dest: '<%= yeoman.tmp %>/l10n'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
grunt.registerTask('scan-email-translations', 'Scan the generated emails for invalid style tags', [
|
||||
'generate-valid-styles',
|
||||
'po2json',
|
||||
|
@ -10,106 +11,4 @@ module.exports = function (grunt) {
|
|||
]
|
||||
);
|
||||
|
||||
grunt.registerMultiTask('verify-styles', 'Verify whether the translated styles are valid', function (){
|
||||
var options = this.options({
|
||||
validStyles: []
|
||||
});
|
||||
var styleArray = options.validStyles;
|
||||
|
||||
var fileArray = this.files[0].src;
|
||||
|
||||
var hasErrors = 0;
|
||||
var styleRegex = /style="[\S\s]*?"/ig;
|
||||
|
||||
fileArray.forEach(function (src) {
|
||||
var json = grunt.file.readJSON(src);
|
||||
var styleList = [];
|
||||
for(var key in json) {
|
||||
if(key.indexOf('style') > 0) {
|
||||
styleList.push(key);
|
||||
}
|
||||
}
|
||||
// Check if there were any styles found, .match() might return null
|
||||
if (styleList !== null) {
|
||||
styleList = styleList.map(stripSpecialChars);
|
||||
// retrieve the style attribute and its contents and store
|
||||
styleList = styleList.map(function (str) {
|
||||
if (styleRegex.test(str)) {
|
||||
str = str.match(styleRegex)[0];
|
||||
return str;
|
||||
}
|
||||
});
|
||||
styleList.forEach(function (style) {
|
||||
if(style !== undefined){
|
||||
if (styleArray.indexOf(style) === -1) {
|
||||
grunt.log.error('Found a style which should not be there', style, src);
|
||||
hasErrors = hasErrors + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (hasErrors !== 0) {
|
||||
grunt.fail.warn('Found ' + hasErrors + ' ' + grunt.util.pluralize(hasErrors, 'error/errors') + ' in ' + fileArray.length + ' files');
|
||||
} else {
|
||||
grunt.log.writeln('Checked ' + fileArray.length + ' files for valid styles and found nothing wrong.');
|
||||
}
|
||||
});
|
||||
|
||||
grunt.config('verify-styles', {
|
||||
dist: {
|
||||
options: {
|
||||
validStyles: grunt.file.readJSON('.tmp/validStyles.json')
|
||||
},
|
||||
files: [
|
||||
{
|
||||
expand: false,
|
||||
src: [
|
||||
'.tmp/l10n/*/*.json'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
grunt.registerMultiTask('generate-valid-styles', 'Generate the list of valid styles for translation', function () {
|
||||
//different regex from above, uses single quotes not double
|
||||
var styleRegex = /style='[\s\S]*?'/ig;
|
||||
var styleArray = [];
|
||||
|
||||
// iterate through .pot file and extract the styles
|
||||
this.files[0].src.forEach(function (src) {
|
||||
var contents = grunt.file.read(src);
|
||||
if (styleRegex.test(contents)) {
|
||||
Array.prototype.push.apply(styleArray, contents.match(styleRegex));
|
||||
}
|
||||
});
|
||||
styleArray = styleArray.map(stripSpecialChars);
|
||||
// write out the styles to a temporary JSON file
|
||||
grunt.file.write(this.files[0].dest, JSON.stringify(styleArray));
|
||||
grunt.log.writeln('Generated styles from ' + this.files[0].src.length + ' files');
|
||||
});
|
||||
|
||||
grunt.config('generate-valid-styles', {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
expand: false,
|
||||
src: [
|
||||
'locale/templates/*/server.pot'
|
||||
],
|
||||
dest: '.tmp/validStyles.json'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
function stripSpecialChars(str) {
|
||||
str = str.replace(/'/g, '"');
|
||||
str = str.replace(/""/g, '');
|
||||
str = str.replace(/\n/g, '');
|
||||
return str.replace(/(\\)/g, '');
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var path = require('path');
|
||||
var stripSpecialChars = require('./lib/strip-special-chars');
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
grunt.registerMultiTask('verify-styles', 'Verify whether the translated styles are valid', function (){
|
||||
var validStylesFile = path.join(this.files[0].dest, grunt.config.process('<%= yeoman.validStylesFile %>'));
|
||||
var validStylesArray = grunt.file.readJSON(validStylesFile);
|
||||
var filesToBeChecked = this.files[0].src;
|
||||
|
||||
var hasErrors = 0;
|
||||
var generatedStylesRegex = /style=('|")[\S\s]*?('|")/ig;
|
||||
|
||||
filesToBeChecked.forEach(function (src) {
|
||||
var jsonContent = grunt.file.readJSON(src);
|
||||
var generatedStylesList = [];
|
||||
for(var key in jsonContent) {
|
||||
if(key.indexOf('style') > 0) {
|
||||
generatedStylesList.push(key);
|
||||
}
|
||||
}
|
||||
// Check if there were any styles found, .match() might return null
|
||||
if (generatedStylesList.length > 0) {
|
||||
generatedStylesList = generatedStylesList.map(stripSpecialChars);
|
||||
// retrieve the style attribute and its contents and store
|
||||
generatedStylesList = generatedStylesList.map(function (str) {
|
||||
if (generatedStylesRegex.test(str)) {
|
||||
var matches = str.match(generatedStylesRegex);
|
||||
var invalidStyles = '';
|
||||
for (var style in matches) {
|
||||
invalidStyles = invalidStyles + matches[style];
|
||||
}
|
||||
return invalidStyles;
|
||||
}
|
||||
});
|
||||
}
|
||||
generatedStylesList.forEach(function (style) {
|
||||
if(style && style.length > 0){
|
||||
if (validStylesArray.indexOf(style) === -1) {
|
||||
grunt.log.error('Found a style which should not be there: %s\n File: %s\n', style, src);
|
||||
hasErrors = hasErrors + 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (hasErrors !== 0) {
|
||||
grunt.fail.warn('Found ' + hasErrors + ' ' + grunt.util.pluralize(hasErrors, 'error/errors') + ' in ' + filesToBeChecked.length + ' files');
|
||||
} else {
|
||||
grunt.log.writeln('Checked ' + filesToBeChecked.length + ' files for valid styles and found nothing wrong.');
|
||||
}
|
||||
});
|
||||
|
||||
grunt.config('verify-styles', {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
expand: false,
|
||||
src: [
|
||||
'<%= yeoman.tmp %>/l10n/**/*.json'
|
||||
],
|
||||
dest: '<%= yeoman.tmp %>'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,76 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
var xss = require('xss');
|
||||
|
||||
module.exports = function (grunt) {
|
||||
'use strict';
|
||||
grunt.registerTask('xss-flag', 'Run the XSS parser on the generated l10n files', [
|
||||
'po2json',
|
||||
'l10n-json-to-html',
|
||||
'xss-parse'
|
||||
]);
|
||||
|
||||
grunt.registerMultiTask('xss-parse', 'Parse HTML files to check for XSS vectors', function () {
|
||||
var interpolatedValueRegex = /^\%\(\w+\)s$/;
|
||||
this.files.forEach(function (file) {
|
||||
// iterate through each HTML file in the .tmp folder
|
||||
var src = file.src[0];
|
||||
var contents = grunt.file.read(src);
|
||||
// whitelist of allowed tags and attributes, nothing else is allowed
|
||||
var options = {
|
||||
whiteList: {
|
||||
a: ['href', 'id', 'style'],
|
||||
em: [],
|
||||
span: ['id', 'tabindex'],
|
||||
strong: []
|
||||
},
|
||||
onTagAttr: function (tag, name, value, isWhiteAttr) {
|
||||
// On encountering any tag, check if the tag has a valid attribute.
|
||||
// If the attribute is valid, check for its value, flag if illegal
|
||||
// Illegal includes any mention of the string 'javascript', case insensitive
|
||||
// Templated strings are allowed i.e strings of the form '%(termsURI)s'
|
||||
// If these are considered to be not secure, we should change
|
||||
if (name !== 'style' &&
|
||||
isWhiteAttr &&
|
||||
xss.safeAttrValue(tag, name, value) === '' &&
|
||||
! interpolatedValueRegex.test(value)) {
|
||||
grunt.log.error('%s: INVALID VALUE FOR ATTRIBUTE <%s %s="%s">', src, tag, name, value);
|
||||
return '';
|
||||
// return '' to replace the invalid attribute with ''
|
||||
}
|
||||
},
|
||||
onIgnoreTag: function (tag, html, options) {
|
||||
// On encountering any illegal tag, flag it
|
||||
grunt.log.error('%s: UNEXPECTED TAGS found <%s>', src, tag);
|
||||
return '';
|
||||
},
|
||||
onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) {
|
||||
// On encountering any illegal attribute, flag it
|
||||
grunt.log.error('%s: UNEXPECTED TAG ATTRIBUTES FOUND <%s %s="">', src, tag, name);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
contents = xss(contents, options);
|
||||
});
|
||||
if (this.errorCount !== 0) {
|
||||
grunt.fail.warn('Found ' + this.errorCount + ' ' + grunt.util.pluralize(this.errorCount, 'error/errors') + ' in ' + this.files.length + ' files');
|
||||
} else {
|
||||
grunt.log.writeln('Checked ' + this.files.length + ' files for XSS vulnerabilities');
|
||||
}
|
||||
});
|
||||
|
||||
grunt.config('xss-parse', {
|
||||
dist: {
|
||||
files: [
|
||||
{
|
||||
expand: true,
|
||||
src: [
|
||||
'<%= yeoman.tmp %>/html/l10n/**/*.html'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,17 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
module.exports = function (grunt) {
|
||||
var TEMP_DIR = '.tmp';
|
||||
var HTML_DIR = 'html';
|
||||
var VALID_STYLES_FILE_NAME = 'valid_styles.json';
|
||||
|
||||
grunt.config('yeoman', {
|
||||
/*eslint-disable camelcase */
|
||||
html: HTML_DIR,
|
||||
strings_src: 'locale',
|
||||
tmp: TEMP_DIR,
|
||||
validStylesFile: VALID_STYLES_FILE_NAME
|
||||
});
|
||||
};
|
|
@ -19,6 +19,7 @@
|
|||
"grunt-po2json": "git://github.com/shane-tomlinson/grunt-po2json.git#aa276503e",
|
||||
"i18n-abide": "0.0.23",
|
||||
"load-grunt-tasks": "3.2.0",
|
||||
"time-grunt": "1.2.1"
|
||||
"time-grunt": "1.2.1",
|
||||
"xss": "0.2.2"
|
||||
}
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче