зеркало из
1
0
Форкнуть 0

feat(build): sanitize HTML translations in l10n

Scan all HTML translations for XSS vectors.
This commit is contained in:
Sai Pc 2015-07-28 16:53:27 -07:00 коммит произвёл Shane Tomlinson
Родитель b14b59f00b
Коммит 8436e58f23
9 изменённых файлов: 286 добавлений и 107 удалений

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

@ -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 %>'
}
]
}
});
};

57
grunttasks/json2html.js Normal file
Просмотреть файл

@ -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'
]
}
]
}
});
};

17
grunttasks/yeoman.js Normal file
Просмотреть файл

@ -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"
}
}