CVE-2020-7649 (published 2022-07-25T14:15Z)

This affects the package snyk-broker before 4.73.0. It allows arbitrary file reads for users with access to Snyk's internal network via directory traversal.

CVSS v3 base score: 4.9, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/snyk/broker
commit_details: repo_url: https://github.com/snyk/broker
author: Tom Robinson
author_date: 2020-03-18 16:25:45+00:00
msg: feat: reject paths with directory traversal
repository: repo_url: https://github.com/snyk/broker
repo_name: snyk/broker
description: A broker system between a public service and a private service
repo_language: TypeScript
owner: snyk
file_changes: filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 const minimatch = require('minimatch');
 const pathRegexp = require('path-to-regexp');
 const qs = require('qs');
+const path = require('path');
 const undefsafe = require('undefsafe');
 const replace = require('../replace-vars');
 const authHeader = require('../auth-header');
@@ -32,7 +33,7 @@ module.exports = ruleSource => {
   // array of entries with
   const tests = rules.map(entry => {
     const keys = [];
-    let { method, origin, path, valid, stream } = entry;
+    let { method, origin, path: entryPath, valid, stream } = entry;
     method = (method || 'get').toLowerCase();
     valid = valid || [];
 
@@ -44,7 +45,7 @@ module.exports = ruleSource => {
     const fromConfig = {};
 
     // slightly bespoke version of replace-vars.js
-    path = (path || '').replace(/(\${.*?})/g, (_, match) => {
+    entryPath = (entryPath || '').replace(/(\${.*?})/g, (_, match) => {
       const key = match.slice(2, -1); // ditch the wrappers
       fromConfig[key] = config[key] || '';
       return ':' + key;
@@ -52,12 +53,12 @@ module.exports = ruleSource => {
 
     origin = replace(origin, config);
 
-    if (path[0] !== '/') {
-      path = '/' + path;
+    if (entryPath[0] !== '/') {
+      entryPath = '/' + entryPath;
     }
 
-    logger.info({ method, path }, 'adding new filter rule');
-    const regexp = pathRegexp(path, keys);
+    logger.info({ method, path: entryPath }, 'adding new filter rule');
+    const regexp = pathRegexp(entryPath, keys);
 
     return (req) => {
       // check the request method
@@ -65,6 +66,11 @@ module.exports = ruleSource => {
         return false;
       }
 
+      // Do not allow directory traversal
+      if (path.normalize(req.url) !== req.url) {
+        return false;
+      }
+
       // Discard any fragments before further processing
       const mainURI = req.url.split('#')[0];
 
@@ -133,7 +139,7 @@ module.exports = ruleSource => {
         }
       }
 
-      logger.debug({ path, origin, url, querystring }, 'rule matched');
+      logger.debug({ path: entryPath, origin, url, querystring }, 'rule matched');
 
       querystring = (querystring) ? `?${querystring}` : '';
       return {

programming_language: TypeScript
filename: github.json
change_type: ModificationType.MODIFY
diff: @@ -8,6 +8,12 @@
       "method": "GET",
       "path": "/repos/:name/:repo/contents/:path*/package.json",
       "origin": "https://${GITHUB_TOKEN}@${GITHUB_API}"
+    },
+    {
+      "//": "used to whitelist a folder",
+      "method": "GET",
+      "path": "/repos/:name/:repo/contents/:path*/docs/*",
+      "origin": "https://${GITHUB_TOKEN}@${GITHUB_API}"
     } 
   ]
 }

programming_language: JavaScript
filename: filters.test.js
change_type: ModificationType.MODIFY
diff: @@ -7,7 +7,7 @@ const jsonBuffer = (body) => Buffer.from(JSON.stringify(body));
 
 test('Filter on URL', t => {
   t.test('for GitHub private filters', (t) => {
-    t.plan(3);
+    t.plan(4);
     
     const ruleSource = require(__dirname + '/../fixtures/accept/github.json');
     const filter = Filters(ruleSource.private);
@@ -39,6 +39,18 @@ test('Filter on URL', t => {
       t.end();
     });
 
+    t.test('should block when path includes directory traversal', (t) => {
+      filter({
+        url: '/repos/angular/angular/contents/path/to/docs/../../sensitive/file.js',
+        method: 'GET',
+      }, (error, res) => {
+        t.equal(error.message, 'blocked', 'has been blocked');
+        t.equal(res, undefined, 'no follow allowed');
+      });
+
+      t.end();
+    });
+
     t.end();
   });
 

programming_language: JavaScript