CVE-2022-25937: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Versions of the package glance before 3.0.9 are vulnerable to Directory Traversal that allows users to read files outside the public root directory. This is related to but distinct from the vulnerability reported in [CVE-2018-3715](https://security.snyk.io/vuln/npm:glance:20180129).
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: 6.5
Published: 2023-02-13T05:15Z
Repository: https://github.com/jarofghosts/glance
Language: JavaScript
Files changed: index.js, test/glance-test-exploit/secret.txt, test/index.js
Commit date: 2023-02-01 21:08:38 -0800
Commit message: Fix path traversal vulnerability

Diff: diff --git a/index.js b/index.js
index ba9bffc..3d084f1 100644
--- a/index.js
+++ b/index.js
@@ -22,11 +22,11 @@ function Glance(options) {
   options = xtend(defaults, options || {})
 
   this.port = options.port
   this.hideindex = options.hideindex
   this.indices = options.indices
-  this.dir = path.normalize(options.dir)
+  this.dir = path.resolve(options.dir)
   this.nodot = options.nodot
 
   return this
 }
 
@@ -70,11 +70,11 @@ Glance.prototype.serveRequest = function Glance$serveRequest(req, res) {
   request.ip = req.socket.remoteAddress
   request.method = req.method.toLowerCase()
   request.response = res
 
   // prevent traversing directories that are parents of the root
-  if (request.fullPath.slice(0, self.dir.length) !== self.dir) {
+  if (path.relative(self.dir, request.fullPath).startsWith('..')) {
     return self.emit('error', 403, request, res)
   }
 
   if (request.method !== 'get') {
     return self.emit('error', 405, request, res)
@@ -191,14 +191,14 @@ function renderPage(title, body, res) {
     .pipe(res)
 }
 
 function errorTitle(errorCode) {
   var mappings = {
-    '404': 'File Not Found',
-    '403': 'Forbidden',
-    '405': 'Method Not Allowed',
-    '500': 'Internal Server Error',
+    404: 'File Not Found',
+    403: 'Forbidden',
+    405: 'Method Not Allowed',
+    500: 'Internal Server Error',
   }
   return mappings[errorCode.toString()]
 }
 
 function createGlance(options) {
diff --git a/test/glance-test-exploit/secret.txt b/test/glance-test-exploit/secret.txt
new file mode 100644
index 0000000..3d518f9
--- /dev/null
+++ b/test/glance-test-exploit/secret.txt
@@ -0,0 +1 @@
+wee
\ No newline at end of file
diff --git a/test/index.js b/test/index.js
index 5755f1c..1d311cc 100644
--- a/test/index.js
+++ b/test/index.js
@@ -1,6 +1,7 @@
 var http = require('http')
+var net = require('net')
 
 var test = require('tape')
 
 var glance = require('../')
 
@@ -72,12 +73,42 @@ test('403s on dir list if configured', function (t) {
 })
 
 test('fails if path traversal is attempted', function (t) {
   t.plan(1)
 
-  http.get('http://localhost:1666/../index.js', function (res) {
-    t.notStrictEqual(res.statusCode, 200)
+  var socket = new net.Socket()
+  socket.connect(1666, 'localhost', function () {
+    socket.on('data', function (data) {
+      var result = data.toString().split('\n')[0]
+      t.equals(result.trim(), 'HTTP/1.1 403 Forbidden')
+      socket.end()
+    })
+    socket.write(`GET /../index.js HTTP/1.1
+Host: localhost
+user-agent: test/1.2.3
+accept: */*
+
+`)
+  })
+})
+
+test('fails if path traversal with conveniently-named directory is attempted', function (t) {
+  t.plan(1)
+
+  var socket = new net.Socket()
+  socket.connect(1666, 'localhost', function () {
+    socket.on('data', function (data) {
+      var result = data.toString().split('\n')[0]
+      t.equals(result.trim(), 'HTTP/1.1 403 Forbidden')
+      socket.end()
+    })
+    socket.write(`GET /../glance-test-exploit/secret.txt HTTP/1.1
+Host: localhost
+user-agent: test/1.2.3
+accept: */*
+
+`)
   })
 })
 
 test('serves index page', function (t) {
   t.plan(2)

Vulnerable code:   this.dir = path.normalize(options.dir)
  if (request.fullPath.slice(0, self.dir.length) !== self.dir) {
    '404': 'File Not Found',
    '403': 'Forbidden',
    '405': 'Method Not Allowed',
    '500': 'Internal Server Error',
  http.get('http://localhost:1666/../index.js', function (res) {
    t.notStrictEqual(res.statusCode, 200)
Fixed code:   this.dir = path.resolve(options.dir)
  if (path.relative(self.dir, request.fullPath).startsWith('..')) {
    404: 'File Not Found',
    403: 'Forbidden',
    405: 'Method Not Allowed',
    500: 'Internal Server Error',
wee
var net = require('net')
  var socket = new net.Socket()
  socket.connect(1666, 'localhost', function () {
    socket.on('data', function (data) {
      var result = data.toString().split('\n')[0]
      t.equals(result.trim(), 'HTTP/1.1 403 Forbidden')
      socket.end()
    })
    socket.write(`GET /../index.js HTTP/1.1
Host: localhost
user-agent: test/1.2.3
accept: */*

`)
  })
})

test('fails if path traversal with conveniently-named directory is attempted', function (t) {
  t.plan(1)

  var socket = new net.Socket()
  socket.connect(1666, 'localhost', function () {
    socket.on('data', function (data) {
      var result = data.toString().split('\n')[0]
      t.equals(result.trim(), 'HTTP/1.1 403 Forbidden')
      socket.end()
    })
    socket.write(`GET /../glance-test-exploit/secret.txt HTTP/1.1
Host: localhost
user-agent: test/1.2.3
accept: */*

`)