safe_pathname.js 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /** @module */
  2. 'use strict'
  3. const path = require('path')
  4. /**
  5. * Regular expressions for Overleaf v2 taken from
  6. * https://github.com/overleaf/internal/blob/f7b287b6a07354000a6b463ca3a5828104e4a811/services/web/app/src/Features/Project/SafePath.js
  7. */
  8. //
  9. // Regex of characters that are invalid in filenames
  10. //
  11. // eslint-disable-next-line no-control-regex
  12. const BAD_CHAR_RX = /[/*\u0000-\u001F\u007F\u0080-\u009F\uD800-\uDFFF]/g
  13. //
  14. // Regex of filename patterns that are invalid ("." ".." and leading/trailing
  15. // whitespace)
  16. //
  17. const BAD_FILE_RX = /(^\.$)|(^\.\.$)|(^\s+)|(\s+$)/g
  18. //
  19. // Put a block on filenames which match javascript property names, as they
  20. // can cause exceptions where the code puts filenames into a hash. This is a
  21. // temporary workaround until the code in other places is made safe against
  22. // property names.
  23. //
  24. // See https://github.com/overleaf/write_latex/wiki/Using-javascript-Objects-as-Maps
  25. //
  26. const BLOCKED_FILE_RX =
  27. /^(prototype|constructor|toString|toLocaleString|valueOf|hasOwnProperty|isPrototypeOf|propertyIsEnumerable|__defineGetter__|__lookupGetter__|__defineSetter__|__lookupSetter__|__proto__)$/
  28. //
  29. // Maximum path length, in characters. This is fairly arbitrary.
  30. //
  31. const MAX_PATH = 1024
  32. /**
  33. * Replace invalid characters and filename patterns in a filename with
  34. * underscores.
  35. */
  36. function cleanPart(filename) {
  37. filename = filename.replace(BAD_CHAR_RX, '_')
  38. filename = filename.replace(BAD_FILE_RX, function (match) {
  39. return new Array(match.length + 1).join('_')
  40. })
  41. return filename
  42. }
  43. /**
  44. * All pathnames in a Snapshot must be clean. We want pathnames that:
  45. *
  46. * 1. are unambiguous (e.g. no `.`s or redundant path separators)
  47. * 2. do not allow directory traversal attacks (e.g. no `..`s or absolute paths)
  48. * 3. do not contain leading/trailing space
  49. * 4. do not contain the character '*' in filenames
  50. *
  51. * We normalise the pathname, split it by the separator and then clean each part
  52. * as a filename
  53. *
  54. * @param {string} pathname
  55. * @return {String}
  56. */
  57. exports.clean = function (pathname) {
  58. pathname = path.normalize(pathname)
  59. pathname = pathname.replace(/\\/g, '/') // workaround for IE
  60. pathname = pathname.replace(/\/+/g, '/') // no multiple slashes
  61. pathname = pathname.replace(/^(\/.*)$/, '_$1') // no leading /
  62. pathname = pathname.replace(/^(.+)\/$/, '$1') // no trailing /
  63. pathname = pathname.replace(/^ *(.*)$/, '$1') // no leading spaces
  64. pathname = pathname.replace(/^(.*[^ ]) *$/, '$1') // no trailing spaces
  65. if (pathname.length === 0) pathname = '_'
  66. pathname = pathname.split('/').map(cleanPart).join('/')
  67. pathname = pathname.replace(BLOCKED_FILE_RX, '@$1')
  68. return pathname
  69. }
  70. /**
  71. * A pathname is clean (see clean) and not too long.
  72. *
  73. * @param {string} pathname
  74. * @return {Boolean}
  75. */
  76. exports.isClean = function pathnameIsClean(pathname) {
  77. return (
  78. exports.clean(pathname) === pathname &&
  79. pathname.length <= MAX_PATH &&
  80. pathname.length > 0
  81. )
  82. }