CVE-2020-15236: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: In Wiki.js before version 2.5.151, directory traversal outside of Wiki.js context is possible when a storage module with local asset cache fetching is enabled. A malicious user can potentially read any file on the file system by crafting a special URL that allows for directory traversal. This is only possible when a storage module implementing local asset cache (e.g Local File System or Git) is enabled and that no web application firewall solution (e.g. cloudflare) strips potentially malicious URLs. Commit 084dcd69d1591586ee4752101e675d5f0ac6dcdc fixes this vulnerability by sanitizing the path before it is passed on to the storage module. The sanitization step removes any directory traversal (e.g. `..` and `.`) sequences as well as invalid filesystem characters from the path. As a workaround, disable any storage module with local asset caching capabilities such as Local File System and Git.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.5
Published: 2020-10-05T15:15Z
Repository: https://github.com/Requarks/wiki
Language: JavaScript
Files changed: server/helpers/page.js
Commit date: 2020-10-03 01:44:57 -0400
Commit message: fix: strip directory traversal sequences from asset paths

Diff: diff --git a/server/helpers/page.js b/server/helpers/page.js
index 5b3079a8..e2e03648 100644
--- a/server/helpers/page.js
+++ b/server/helpers/page.js
@@ -3,10 +3,12 @@ const _ = require('lodash')
 const crypto = require('crypto')
 const path = require('path')
 
 const localeSegmentRegex = /^[A-Z]{2}(-[A-Z]{2})?$/i
 const localeFolderRegex = /^([a-z]{2}(?:-[a-z]{2})?\/)?(.*)/i
+// eslint-disable-next-line no-control-regex
+const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
 
 const contentToExt = {
   markdown: 'md',
   html: 'html'
 }
@@ -28,14 +30,18 @@ module.exports = {
     }
 
     // Clean Path
     rawPath = _.trim(qs.unescape(rawPath))
     if (_.startsWith(rawPath, '/')) { rawPath = rawPath.substring(1) }
+    rawPath = rawPath.replace(unsafeCharsRegex, '')
     if (rawPath === '') { rawPath = 'home' }
 
     // Extract Info
-    let pathParts = _.filter(_.split(rawPath, '/'), p => !_.isEmpty(p))
+    let pathParts = _.filter(_.split(rawPath, '/'), p => {
+      p = _.trim(p)
+      return !_.isEmpty(p) && p !== '..' && p !== '.'
+    })
     if (pathParts[0].length === 1) {
       pathParts.shift()
     }
     if (localeSegmentRegex.test(pathParts[0])) {
       pathObj.locale = pathParts[0]
@@ -71,11 +77,11 @@ module.exports = {
       ['description', page.description],
       ['published', page.isPublished.toString()],
       ['date', page.updatedAt],
       ['tags', page.tags ? page.tags.map(t => t.tag).join(', ') : ''],
       ['editor', page.editorKey],
-      ['dateCreated', page.createdAt],
+      ['dateCreated', page.createdAt]
     ]
     switch (page.contentType) {
       case 'markdown':
         return '---\n' + meta.map(mt => `${mt[0]}: ${mt[1]}`).join('\n') + '\n---\n\n' + page.content
       case 'html':

Vulnerable code:     let pathParts = _.filter(_.split(rawPath, '/'), p => !_.isEmpty(p))
      ['dateCreated', page.createdAt],
Fixed code: // eslint-disable-next-line no-control-regex
const unsafeCharsRegex = /[\x00-\x1f\x80-\x9f\\"|<>:*?]/
    rawPath = rawPath.replace(unsafeCharsRegex, '')
    let pathParts = _.filter(_.split(rawPath, '/'), p => {
      p = _.trim(p)
      return !_.isEmpty(p) && p !== '..' && p !== '.'
    })
      ['dateCreated', page.createdAt]