VYPR
Moderate severityNVD Advisory· Published Jul 3, 2018· Updated Sep 17, 2024

CVE-2018-3748

CVE-2018-3748

Description

A stored cross-site scripting (XSS) vulnerability in glance node module <=3.0.5 allows arbitrary JavaScript execution via crafted filenames in directory listings.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A stored cross-site scripting (XSS) vulnerability in glance node module <=3.0.5 allows arbitrary JavaScript execution via crafted filenames in directory listings.

Vulnerability

A stored cross-site scripting (XSS) vulnerability exists in the glance node module versions 3.0.5 and earlier [1][4]. The issue occurs in the directory listing functionality when generating HTML for file names. If a file name contains malicious HTML, such as an ` element or a javascript: protocol handler in an element, it is rendered unsanitized in the directory listing page that any user can view. The vulnerability is reachable when the server is configured to serve directory listings (i.e., hideindex` option is false or not set) [2].

Exploitation

An attacker needs the ability to create or rename a file within the served directory (write access or file upload capability). The attacker places a file with a crafted name containing HTML or JavaScript code. When a victim user opens the directory listing page provided by the glance server, the malicious file name is rendered in the browser without proper sanitization, causing the embedded script to execute in the victim's session context [1][3]. No additional user interaction beyond visiting the directory listing is required.

Impact

Successful exploitation allows the attacker to execute arbitrary JavaScript in the context of the victim's browser. This can lead to session hijacking, data exfiltration, defacement, or other actions that the victim's browser can perform against the glance server origin. The scope of compromise is limited to the victim's browser session, but it does not grant direct server-side code execution [1].

Mitigation

The vulnerability is fixed in glance version 3.0.8 [4]. Users should upgrade to version 3.0.8 or later. The fix introduces transformHref and transformLinkText functions that properly encode HTML entities and URL-encode file names, preventing the injection of malicious markup [3]. For users who cannot upgrade, an immediate workaround is to enable the --hideindex option (or set "hideindex": true in the configuration file) to disable directory listing entirely; however, this may break normal site functionality [2].

AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
glancenpm
< 3.0.83.0.8

Affected products

1

Patches

1
cdc68bb68d78

fix filename xss

https://github.com/jarofghosts/glancejesse keaneJul 18, 2020via ghsa
8 files changed · +147 74
  • bin/glance.js+2 2 modified
    @@ -25,7 +25,7 @@ var noptions = {
       port: Number,
       verbose: Boolean,
       help: Boolean,
    -  version: Boolean
    +  version: Boolean,
     }
     
     var shorts = {
    @@ -36,7 +36,7 @@ var shorts = {
       p: ['--port'],
       v: ['--verbose'],
       h: ['--help'],
    -  V: ['--version']
    +  V: ['--version'],
     }
     
     var glanceVersion = require('../package.json').version
    
  • index.js+17 9 modified
    @@ -35,13 +35,13 @@ Glance.prototype = Object.create(EE.prototype)
     Glance.prototype.start = function Glance$start() {
       var self = this
     
    -  self.server = http.createServer(function(req, res) {
    +  self.server = http.createServer(function (req, res) {
         self.serveRequest(req, res)
       })
     
       self.server.listen(self.port, emitStarted)
     
    -  self.server.addListener('connection', function(con) {
    +  self.server.addListener('connection', function (con) {
         con.setTimeout(500)
       })
     
    @@ -82,7 +82,7 @@ Glance.prototype.serveRequest = function Glance$serveRequest(req, res) {
     
       if (
         self.nodot &&
    -    request.fullPath.split(path.sep).some(function(dir) {
    +    request.fullPath.split(path.sep).some(function (dir) {
           return dir.startsWith('.')
         })
       ) {
    @@ -139,13 +139,21 @@ Glance.prototype.serveRequest = function Glance$serveRequest(req, res) {
     
           var listingHtml = '<h3>Directory Listing</h3>'
     
    -      var listing = htmlls(listPath, {hideDot: self.nodot})
    +      var listing = htmlls(listPath, {
    +        hideDot: self.nodot,
    +        transformHref: function (str) {
    +          return encodeURI(str)
    +        },
    +        transformLinkText: function (str) {
    +          return str.replace(/\</g, '&lt;').replace(/\>/g, '&gt;')
    +        },
    +      })
     
    -      listing.on('data', function(buf) {
    +      listing.on('data', function (buf) {
             listingHtml += buf.toString()
           })
     
    -      listing.on('end', function() {
    +      listing.on('end', function () {
             renderPage('Directory Listing', listingHtml, res)
           })
     
    @@ -163,11 +171,11 @@ function showError(errorCode, req, res) {
         path.join(__dirname, 'errors', errorCode + '.html')
       )
     
    -  errorPage.on('data', function(buf) {
    +  errorPage.on('data', function (buf) {
         errorHtml += buf.toString()
       })
     
    -  errorPage.on('end', function() {
    +  errorPage.on('end', function () {
         var title = errorTitle(errorCode)
         renderPage(title, errorHtml, res)
       })
    @@ -188,7 +196,7 @@ function errorTitle(errorCode) {
         '404': 'File Not Found',
         '403': 'Forbidden',
         '405': 'Method Not Allowed',
    -    '500': 'Internal Server Error'
    +    '500': 'Internal Server Error',
       }
       return mappings[errorCode.toString()]
     }
    
  • lib/config.js+1 1 modified
    @@ -4,5 +4,5 @@ module.exports = {
       indices: ['index.html', 'index.htm'],
       dir: process.cwd(),
       verbose: false,
    -  nodot: false
    +  nodot: false,
     }
    
  • package.json+1 1 modified
    @@ -29,7 +29,7 @@
       "dependencies": {
         "bash-color": "0.0.3",
         "filed": "0.1.0",
    -    "html-ls": "1.0.0",
    +    "html-ls": "2.1.0",
         "mime": "1.4.1",
         "nopt": "3.0.4",
         "stream-replace": "1.0.0",
    
  • package-lock.json+73 27 modified
    @@ -30,9 +30,9 @@
           "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
         },
         "acorn": {
    -      "version": "6.1.0",
    -      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.0.tgz",
    -      "integrity": "sha512-MW/FjM+IvU9CgBzjO3UIPCE2pyEwUsoFl+VGdczOPEdxfGFjuKny/gN54mOuX7Qxmb9Rg9MCn2oKiSUeW+pjrw==",
    +      "version": "6.4.1",
    +      "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
    +      "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
           "dev": true
         },
         "acorn-jsx": {
    @@ -169,6 +169,11 @@
           "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
           "dev": true
         },
    +    "core-util-is": {
    +      "version": "1.0.2",
    +      "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
    +      "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
    +    },
         "cross-spawn": {
           "version": "6.0.5",
           "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
    @@ -295,10 +300,21 @@
           }
         },
         "eslint-utils": {
    -      "version": "1.3.1",
    -      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
    -      "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
    -      "dev": true
    +      "version": "1.4.3",
    +      "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
    +      "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
    +      "dev": true,
    +      "requires": {
    +        "eslint-visitor-keys": "^1.1.0"
    +      },
    +      "dependencies": {
    +        "eslint-visitor-keys": {
    +          "version": "1.3.0",
    +          "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz",
    +          "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==",
    +          "dev": true
    +        }
    +      }
         },
         "eslint-visitor-keys": {
           "version": "1.0.0",
    @@ -478,12 +494,12 @@
           "dev": true
         },
         "html-ls": {
    -      "version": "1.0.0",
    -      "resolved": "https://registry.npmjs.org/html-ls/-/html-ls-1.0.0.tgz",
    -      "integrity": "sha1-5EGHXZZEjyfqbjhFDu1dyeT2NHA=",
    +      "version": "2.1.0",
    +      "resolved": "https://registry.npmjs.org/html-ls/-/html-ls-2.1.0.tgz",
    +      "integrity": "sha512-JZkKBHTc0ggSoS3HZbyE+rt6/kv4KvAQGsxvGwgQBgEsLZH2Nsu9KTj7xOuDQyrGElmYJSfmxvkM11UGaBIVpQ==",
           "requires": {
             "ls-stream": "1.0.0",
    -        "through": "2.3.4"
    +        "through2": "^0.6.5"
           }
         },
         "iconv-lite": {
    @@ -530,8 +546,7 @@
         "inherits": {
           "version": "2.0.3",
           "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
    -      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
    -      "dev": true
    +      "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
         },
         "inquirer": {
           "version": "6.2.2",
    @@ -589,6 +604,11 @@
           "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
           "dev": true
         },
    +    "isarray": {
    +      "version": "0.0.1",
    +      "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
    +      "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
    +    },
         "isexe": {
           "version": "2.0.0",
           "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
    @@ -602,9 +622,9 @@
           "dev": true
         },
         "js-yaml": {
    -      "version": "3.12.1",
    -      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz",
    -      "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==",
    +      "version": "3.14.0",
    +      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz",
    +      "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==",
           "dev": true,
           "requires": {
             "argparse": "^1.0.7",
    @@ -634,9 +654,9 @@
           }
         },
         "lodash": {
    -      "version": "4.17.11",
    -      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
    -      "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
    +      "version": "4.17.19",
    +      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
    +      "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
           "dev": true
         },
         "ls-stream": {
    @@ -675,18 +695,18 @@
           }
         },
         "minimist": {
    -      "version": "0.0.8",
    -      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
    -      "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
    +      "version": "1.2.5",
    +      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
    +      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
           "dev": true
         },
         "mkdirp": {
    -      "version": "0.5.1",
    -      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
    -      "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
    +      "version": "0.5.5",
    +      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
    +      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
           "dev": true,
           "requires": {
    -        "minimist": "0.0.8"
    +        "minimist": "^1.2.5"
           }
         },
         "ms": {
    @@ -810,6 +830,17 @@
           "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
           "dev": true
         },
    +    "readable-stream": {
    +      "version": "1.0.34",
    +      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
    +      "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
    +      "requires": {
    +        "core-util-is": "~1.0.0",
    +        "inherits": "~2.0.1",
    +        "isarray": "0.0.1",
    +        "string_decoder": "~0.10.x"
    +      }
    +    },
         "regexpp": {
           "version": "2.0.1",
           "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
    @@ -949,6 +980,11 @@
             "strip-ansi": "^4.0.0"
           }
         },
    +    "string_decoder": {
    +      "version": "0.10.31",
    +      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
    +      "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
    +    },
         "strip-ansi": {
           "version": "4.0.0",
           "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
    @@ -1047,7 +1083,17 @@
         "through": {
           "version": "2.3.4",
           "resolved": "https://registry.npmjs.org/through/-/through-2.3.4.tgz",
    -      "integrity": "sha1-SV5A6Nio6uvHwnXqiMK4/BTFZFU="
    +      "integrity": "sha1-SV5A6Nio6uvHwnXqiMK4/BTFZFU=",
    +      "dev": true
    +    },
    +    "through2": {
    +      "version": "0.6.5",
    +      "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz",
    +      "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=",
    +      "requires": {
    +        "readable-stream": ">=1.0.33-1 <1.1.0-0",
    +        "xtend": ">=4.0.0 <4.1.0-0"
    +      }
         },
         "tmp": {
           "version": "0.0.33",
    
  • test/glance-test/test1/<img src=x onerror=alert(1)>+0 0 added
  • test/index.js+46 27 modified
    @@ -6,106 +6,106 @@ var glance = require('../')
     
     var glanceServer = glance({port: 1666, dir: './test/glance-test'})
     
    -test('doesnt explode immediately', function(t) {
    +test('doesnt explode immediately', function (t) {
       t.plan(1)
     
    -  t.doesNotThrow(function() {
    +  t.doesNotThrow(function () {
         glanceServer.start()
       })
     })
     
    -test('serves plaintext with headers', function(t) {
    -  http.get('http://localhost:1666/file.txt', function(res) {
    +test('serves plaintext with headers', function (t) {
    +  http.get('http://localhost:1666/file.txt', function (res) {
         t.plan(3)
     
         var text = ''
     
         t.strictEqual(res.statusCode, 200)
         t.strictEqual(res.headers['content-type'], 'text/plain')
     
    -    res.on('data', function(data) {
    +    res.on('data', function (data) {
           text += data
         })
     
    -    res.on('end', function() {
    +    res.on('end', function () {
           t.strictEqual(text, 'howdy!')
         })
       })
     })
     
    -test('deals with uri encoding', function(t) {
    -  http.get('http://localhost:1666/file%20with%20space.html', function(res) {
    +test('deals with uri encoding', function (t) {
    +  http.get('http://localhost:1666/file%20with%20space.html', function (res) {
         t.plan(3)
     
         var uritext = ''
     
         t.strictEqual(res.statusCode, 200)
         t.strictEqual(res.headers['content-type'], 'text/html')
     
    -    res.on('data', function(data) {
    +    res.on('data', function (data) {
           uritext += data
         })
     
    -    res.on('end', function() {
    +    res.on('end', function () {
           t.strictEqual(uritext, 'hey, now!')
         })
       })
     })
     
    -test('404s on file not found', function(t) {
    +test('404s on file not found', function (t) {
       t.plan(1)
     
    -  http.get('http://localhost:1666/nofile.md', function(res) {
    +  http.get('http://localhost:1666/nofile.md', function (res) {
         t.strictEqual(res.statusCode, 404)
       })
     })
     
    -test('403s on dir list if configured', function(t) {
    +test('403s on dir list if configured', function (t) {
       t.plan(1)
     
       glanceServer.hideindex = true
     
    -  http.get('http://localhost:1666/', function(res) {
    +  http.get('http://localhost:1666/', function (res) {
         glanceServer.hideindex = false
         t.strictEqual(res.statusCode, 403)
       })
     })
     
    -test('fails if path traversal is attempted', function(t) {
    +test('fails if path traversal is attempted', function (t) {
       t.plan(1)
     
    -  http.get('http://localhost:1666/../index.js', function(res) {
    +  http.get('http://localhost:1666/../index.js', function (res) {
         t.notStrictEqual(res.statusCode, 200)
       })
     })
     
    -test('serves index page', function(t) {
    +test('serves index page', function (t) {
       t.plan(2)
     
       var data = []
     
    -  http.get('http://localhost:1666/', function(res) {
    +  http.get('http://localhost:1666/', function (res) {
         t.strictEqual(res.statusCode, 200)
     
         res.on('data', data.push.bind(data))
     
    -    res.on('end', function() {
    +    res.on('end', function () {
           t.strictEqual(data.join(''), 'wee\n')
         })
       })
     })
     
    -test('405s on everything but GET', function(t) {
    +test('405s on everything but GET', function (t) {
       t.plan(3)
     
       var badMethods = ['POST', 'DELETE', 'PUT']
     
    -  badMethods.forEach(function(method) {
    +  badMethods.forEach(function (method) {
         var options = {
           host: 'localhost',
           port: 1666,
           path: '/file.txt',
    -      method: method
    +      method: method,
         }
     
         var req = http.request(options, verifyCode)
    @@ -118,22 +118,41 @@ test('405s on everything but GET', function(t) {
       }
     })
     
    -test('serves files from within dot dirs by default', function(t) {
    +test('serves files from within dot dirs by default', function (t) {
       t.plan(2)
     
    -  http.get('http://localhost:1666/.test/whatever.txt', function(res) {
    +  http.get('http://localhost:1666/.test/whatever.txt', function (res) {
         t.strictEqual(res.statusCode, 200)
       })
     
    -  http.get('http://localhost:1666/test1/.test2/lol.txt', function(res) {
    +  http.get('http://localhost:1666/test1/.test2/lol.txt', function (res) {
         t.strictEqual(res.statusCode, 200)
       })
     })
     
    -test('shuts down without exploding', function(t) {
    +test('sanitizes filenames', function (t) {
    +  t.plan(3)
    +
    +  var data = []
    +
    +  http.get('http://localhost:1666/test1/', function (res) {
    +    res.on('data', function (chunk) {
    +      data.push(chunk)
    +    })
    +    res.on('end', function () {
    +      var response = data.join('')
    +
    +      t.notOk(/\<img/.test(response), 'raw image tag is not on page')
    +      t.ok(/\&lt;img/.test(response), 'name is replaced with html escape')
    +      t.ok(/\%3Cimg/.test(response), 'link is replaced with uriEncode')
    +    })
    +  })
    +})
    +
    +test('shuts down without exploding', function (t) {
       t.plan(1)
     
    -  t.doesNotThrow(function() {
    +  t.doesNotThrow(function () {
         glanceServer.stop()
       })
     })
    
  • test/nodot.js+7 7 modified
    @@ -6,30 +6,30 @@ var glance = require('../')
     
     var glanceServer = glance({port: 1666, dir: './test/glance-test', nodot: true})
     
    -test('doesnt explode immediately', function(t) {
    +test('doesnt explode immediately', function (t) {
       t.plan(1)
     
    -  t.doesNotThrow(function() {
    +  t.doesNotThrow(function () {
         glanceServer.start()
       })
     })
     
    -test('404s if dot dir with nodot', function(t) {
    +test('404s if dot dir with nodot', function (t) {
       t.plan(2)
     
    -  http.get('http://localhost:1666/.test/whatever.txt', function(res) {
    +  http.get('http://localhost:1666/.test/whatever.txt', function (res) {
         t.strictEqual(res.statusCode, 404)
       })
     
    -  http.get('http://localhost:1666/test1/.test2/lol.txt', function(res) {
    +  http.get('http://localhost:1666/test1/.test2/lol.txt', function (res) {
         t.strictEqual(res.statusCode, 404)
       })
     })
     
    -test('shuts down without exploding', function(t) {
    +test('shuts down without exploding', function (t) {
       t.plan(1)
     
    -  t.doesNotThrow(function() {
    +  t.doesNotThrow(function () {
         glanceServer.stop()
       })
     })
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.