Merge pull request #6523 from overleaf/jpa-translations-check-sanitize

[web] scripts/translations: add script for checking html sanitization

GitOrigin-RevId: d4b9c9a7eb1ed0ca9202b0cb6e4c33f3e73bd0e4
This commit is contained in:
Timothée Alby 2022-02-02 11:24:08 +01:00 committed by Copybot
parent 64423936b9
commit 58cf92620a
3 changed files with 88 additions and 43 deletions

View file

@ -0,0 +1,42 @@
const Path = require('path')
const fs = require('fs')
const { sanitize } = require('./sanitize')
async function main() {
let ok = true
const base = Path.join(__dirname, '/../../locales')
for (const name of await fs.promises.readdir(base)) {
if (name === 'README.md') continue
const blob = await fs.promises.readFile(
Path.join(__dirname, '/../../locales', name),
'utf-8'
)
const locales = JSON.parse(blob)
for (const key of Object.keys(locales)) {
const want = locales[key]
const got = sanitize(locales[key])
if (got !== want) {
if (want === 'Editor & PDF' && got === 'Editor & PDF') {
// Ignore this mismatch. React cannot handle escaped labels.
continue
}
ok = false
console.warn(`${name}: ${key}: want: ${want}`)
console.warn(`${name}: ${key}: got: ${got}`)
}
}
}
if (!ok) {
throw new Error('Check the logs, some values changed.')
}
}
main()
.then(() => {
process.exit(0)
})
.catch(error => {
console.error(error)
process.exit(1)
})

View file

@ -1,7 +1,7 @@
const path = require('path') const path = require('path')
const { promises: fs } = require('fs') const { promises: fs } = require('fs')
const oneSky = require('@brainly/onesky-utils') const oneSky = require('@brainly/onesky-utils')
const sanitizeHtml = require('sanitize-html') const { sanitize } = require('./sanitize')
const { withAuth } = require('./config') const { withAuth } = require('./config')
async function run() { async function run() {
@ -46,45 +46,3 @@ async function run() {
} }
} }
run() run()
/**
* Sanitize a translation string to prevent injection attacks
*
* @param {string} input
* @returns {string}
*/
function sanitize(input) {
// Block Angular XSS
// Ticket: https://github.com/overleaf/issues/issues/4478
input = input.replace(/'/g, '')
// Use left quote where (likely) appropriate.
input.replace(/ /g, ' ')
return sanitizeHtml(input, {
// Allow "replacement" tags (in the format <0>, <1>, <2>, etc) used by
// react-i18next to allow for HTML insertion via the Trans component.
// See: https://github.com/overleaf/developer-manual/blob/master/code/translations.md
// Unfortunately the sanitizeHtml library does not accept regexes or a
// function for the allowedTags option, so we are limited to a hard-coded
// number of "replacement" tags.
allowedTags: ['b', 'strong', 'a', 'code', ...range(10)],
allowedAttributes: {
a: ['href', 'class'],
},
textFilter(text) {
return text
.replace(/\{\{/, '&#123;&#123;')
.replace(/\}\}/, '&#125;&#125;')
},
})
}
/**
* Generate a range of numbers as strings up to the given size
*
* @param {number} size Size of range
* @returns {string[]}
*/
function range(size) {
return Array.from(Array(size).keys()).map(n => n.toString())
}

View file

@ -0,0 +1,45 @@
const sanitizeHtml = require('sanitize-html')
/**
* Sanitize a translation string to prevent injection attacks
*
* @param {string} input
* @returns {string}
*/
function sanitize(input) {
// Block Angular XSS
// Ticket: https://github.com/overleaf/issues/issues/4478
input = input.replace(/'/g, '')
// Use left quote where (likely) appropriate.
input.replace(/ /g, ' ')
return sanitizeHtml(input, {
// Allow "replacement" tags (in the format <0>, <1>, <2>, etc) used by
// react-i18next to allow for HTML insertion via the Trans component.
// See: https://github.com/overleaf/developer-manual/blob/master/code/translations.md
// Unfortunately the sanitizeHtml library does not accept regexes or a
// function for the allowedTags option, so we are limited to a hard-coded
// number of "replacement" tags.
allowedTags: ['b', 'strong', 'a', 'code', ...range(10)],
allowedAttributes: {
a: ['href', 'class'],
},
textFilter(text) {
return text
.replace(/\{\{/, '&#123;&#123;')
.replace(/\}\}/, '&#125;&#125;')
},
})
}
/**
* Generate a range of numbers as strings up to the given size
*
* @param {number} size Size of range
* @returns {string[]}
*/
function range(size) {
return Array.from(Array(size).keys()).map(n => n.toString())
}
module.exports = { sanitize }