143 lines
4.0 KiB
JavaScript
143 lines
4.0 KiB
JavaScript
// @ts-check
|
|
'use strict'
|
|
|
|
const path = require('path-browserify')
|
|
|
|
/**
|
|
* Regular expressions for Overleaf v2 taken from
|
|
* https://github.com/overleaf/internal/blob/f7b287b6a07354000a6b463ca3a5828104e4a811/services/web/app/src/Features/Project/SafePath.js
|
|
*/
|
|
|
|
//
|
|
// Regex of characters that are invalid in filenames
|
|
//
|
|
// eslint-disable-next-line no-control-regex
|
|
const BAD_CHAR_RX = /[/*\u0000-\u001F\u007F\u0080-\u009F\uD800-\uDFFF]/g
|
|
|
|
//
|
|
// Regex of filename patterns that are invalid ("." ".." and leading/trailing
|
|
// whitespace)
|
|
//
|
|
const BAD_FILE_RX = /(^\.$)|(^\.\.$)|(^\s+)|(\s+$)/g
|
|
|
|
//
|
|
// Put a block on filenames which match javascript property names, as they
|
|
// can cause exceptions where the code puts filenames into a hash. This is a
|
|
// temporary workaround until the code in other places is made safe against
|
|
// property names.
|
|
//
|
|
// See https://github.com/overleaf/write_latex/wiki/Using-javascript-Objects-as-Maps
|
|
//
|
|
const BLOCKED_FILE_RX =
|
|
/^(prototype|constructor|toString|toLocaleString|valueOf|hasOwnProperty|isPrototypeOf|propertyIsEnumerable|__defineGetter__|__lookupGetter__|__defineSetter__|__lookupSetter__|__proto__)$/
|
|
|
|
//
|
|
// Maximum path length, in characters. This is fairly arbitrary.
|
|
//
|
|
const MAX_PATH = 1024
|
|
|
|
/**
|
|
* Replace invalid characters and filename patterns in a filename with
|
|
* underscores.
|
|
* @param {string} filename
|
|
*/
|
|
function cleanPart(filename) {
|
|
filename = filename.replace(BAD_CHAR_RX, '_')
|
|
filename = filename.replace(BAD_FILE_RX, function (match) {
|
|
return new Array(match.length + 1).join('_')
|
|
})
|
|
return filename
|
|
}
|
|
|
|
/**
|
|
* All pathnames in a Snapshot must be clean. We want pathnames that:
|
|
*
|
|
* 1. are unambiguous (e.g. no `.`s or redundant path separators)
|
|
* 2. do not allow directory traversal attacks (e.g. no `..`s or absolute paths)
|
|
* 3. do not contain leading/trailing space
|
|
* 4. do not contain the character '*' in filenames
|
|
*
|
|
* We normalise the pathname, split it by the separator and then clean each part
|
|
* as a filename
|
|
*
|
|
* @param {string} pathname
|
|
* @return {String}
|
|
*/
|
|
exports.clean = function (pathname) {
|
|
return exports.cleanDebug(pathname)[0]
|
|
}
|
|
|
|
/**
|
|
* See clean
|
|
* @param {string} pathname
|
|
* @return {[string,string]}
|
|
*/
|
|
exports.cleanDebug = function (pathname) {
|
|
let prev = pathname
|
|
let reason = ''
|
|
|
|
/**
|
|
* @param {string} label
|
|
*/
|
|
function recordReasonIfChanged(label) {
|
|
if (pathname === prev) return
|
|
if (reason) reason += ','
|
|
reason += label
|
|
prev = pathname
|
|
}
|
|
pathname = path.normalize(pathname)
|
|
recordReasonIfChanged('normalize')
|
|
|
|
pathname = pathname.replace(/\\/g, '/')
|
|
recordReasonIfChanged('workaround for IE')
|
|
|
|
pathname = pathname.replace(/\/+/g, '/')
|
|
recordReasonIfChanged('no multiple slashes')
|
|
|
|
pathname = pathname.replace(/^(\/.*)$/, '_$1')
|
|
recordReasonIfChanged('no leading /')
|
|
|
|
pathname = pathname.replace(/^(.+)\/$/, '$1')
|
|
recordReasonIfChanged('no trailing /')
|
|
|
|
pathname = pathname.replace(/^ *(.*)$/, '$1')
|
|
recordReasonIfChanged('no leading spaces')
|
|
|
|
pathname = pathname.replace(/^(.*[^ ]) *$/, '$1')
|
|
recordReasonIfChanged('no trailing spaces')
|
|
|
|
if (pathname.length === 0) pathname = '_'
|
|
recordReasonIfChanged('empty')
|
|
|
|
pathname = pathname.split('/').map(cleanPart).join('/')
|
|
recordReasonIfChanged('cleanPart')
|
|
|
|
pathname = pathname.replace(BLOCKED_FILE_RX, '@$1')
|
|
recordReasonIfChanged('BLOCKED_FILE_RX')
|
|
return [pathname, reason]
|
|
}
|
|
|
|
/**
|
|
* A pathname is clean (see clean) and not too long.
|
|
*
|
|
* @param {string} pathname
|
|
* @return {Boolean}
|
|
*/
|
|
exports.isClean = function pathnameIsClean(pathname) {
|
|
return exports.isCleanDebug(pathname)[0]
|
|
}
|
|
|
|
/**
|
|
* A pathname is clean (see clean) and not too long.
|
|
*
|
|
* @param {string} pathname
|
|
* @return {[boolean,string]}
|
|
*/
|
|
exports.isCleanDebug = function (pathname) {
|
|
if (pathname.length > MAX_PATH) return [false, 'MAX_PATH']
|
|
if (pathname.length === 0) return [false, 'empty']
|
|
const [cleanPathname, reason] = exports.cleanDebug(pathname)
|
|
if (cleanPathname !== pathname) return [false, reason]
|
|
return [true, '']
|
|
}
|