CVE-2022-31506: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: The cmusatyalab/opendiamond repository through 10.1.1 on GitHub allows absolute path traversal because the Flask send_file function is used unsafely.
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: 9.3
Published: 2022-07-11T01:15Z
Repository: https://github.com/cmusatyalab/opendiamond
Language: Python
Files changed: opendiamond/dataretriever/augment_store.py, opendiamond/dataretriever/diamond_store.py, opendiamond/dataretriever/mixer_store.py, opendiamond/dataretriever/test_store.py, opendiamond/dataretriever/video_store.py, opendiamond/dataretriever/yfcc100m_mysql_store.py
Commit date: 2022-05-04 09:57:21 -0400
Commit message: Fix path traversal issues in all dataretriever backends

Diff: diff --git a/opendiamond/dataretriever/augment_store.py b/opendiamond/dataretriever/augment_store.py
index 00ed5bff..5d8cd8dd 100644
--- a/opendiamond/dataretriever/augment_store.py
+++ b/opendiamond/dataretriever/augment_store.py
@@ -22,11 +22,11 @@ import random
 import glob
 from itertools import cycle
 from flask import Blueprint, url_for, Response, stream_with_context, send_file, \
     jsonify
 from werkzeug.datastructures import Headers
-from werkzeug.utils import safe_join
+from werkzeug.security import safe_join
 from opendiamond.dataretriever.util import read_file_list, write_data
 
 
 BASEURL = 'augment'
 STYLE = False
diff --git a/opendiamond/dataretriever/diamond_store.py b/opendiamond/dataretriever/diamond_store.py
index 0bcb1160..27c75184 100644
--- a/opendiamond/dataretriever/diamond_store.py
+++ b/opendiamond/dataretriever/diamond_store.py
@@ -15,10 +15,11 @@ import datetime
 from xml.sax.saxutils import quoteattr
 
 from flask import Blueprint, url_for, Response, stream_with_context, send_file, \
     jsonify
 from werkzeug.datastructures import Headers
+from werkzeug.security import safe_join
 
 from opendiamond.dataretriever.util import ATTR_SUFFIX
 
 BASEURL = 'collection'
 STYLE = False
@@ -118,15 +119,15 @@ def _get_object_src_uri(object_path):
     else:
         return url_for('.get_object_src_http', obj_path=object_path)
 
 
 def _get_obj_absolute_path(obj_path):
-    return os.path.join(DATAROOT, obj_path)
+    return safe_join(DATAROOT, obj_path)
 
 
 def _get_index_absolute_path(index):
-    return os.path.join(INDEXDIR, index)
+    return safe_join(INDEXDIR, index)
 
 
 @scope_blueprint.route('/obj/<path:obj_path>')
 def get_object_src_http(obj_path):
     path = _get_obj_absolute_path(obj_path)
diff --git a/opendiamond/dataretriever/mixer_store.py b/opendiamond/dataretriever/mixer_store.py
index f816d870..a9b55ae0 100644
--- a/opendiamond/dataretriever/mixer_store.py
+++ b/opendiamond/dataretriever/mixer_store.py
@@ -22,10 +22,11 @@ import random
 import glob
 from itertools import cycle
 from flask import Blueprint, url_for, Response, stream_with_context, send_file, \
     jsonify
 from werkzeug.datastructures import Headers
+from werkzeug.security import safe_join
 
 BASEURL = 'cocktail'
 STYLE = False
 LOCAL_OBJ_URI = True  # if true, return local file path, otherwise http.
 INDEXDIR = DATAROOT = None
@@ -247,15 +248,15 @@ def _get_object_src_uri(object_path):
 
 def _get_obj_path(obj_path):
     return obj_path.replace(DATAROOT+'/', '')
 
 def _get_obj_absolute_path(obj_path):
-    return os.path.join(DATAROOT, obj_path)
+    return safe_join(DATAROOT, obj_path)
 
 
 def _get_index_absolute_path(index):
-    return os.path.join(INDEXDIR, index)
+    return safe_join(INDEXDIR, index)
 
 
 @scope_blueprint.route('/obj/<path:obj_path>')
 def get_object_src_http(obj_path):
     path = _get_obj_absolute_path(obj_path)
diff --git a/opendiamond/dataretriever/test_store.py b/opendiamond/dataretriever/test_store.py
index 0b5d7307..c3dbbcb8 100644
--- a/opendiamond/dataretriever/test_store.py
+++ b/opendiamond/dataretriever/test_store.py
@@ -22,10 +22,12 @@ import random
 import glob
 from itertools import cycle
 from flask import Blueprint, url_for, Response, stream_with_context, send_file, \
     jsonify
 from werkzeug.datastructures import Headers
+from werkzeug.security import safe_join
+
 from opendiamond.dataretriever.test_utils import *
 
 
 BASEURL = 'cocktailtest'
 STYLE = False
@@ -277,15 +279,15 @@ def _get_object_src_uri(object_path):
 
 def _get_obj_path(obj_path):
     return obj_path.replace(DATAROOT+'/', '')
 
 def _get_obj_absolute_path(obj_path):
-    return os.path.join(DATAROOT, obj_path)
+    return safe_join(DATAROOT, obj_path)
 
 
 def _get_index_absolute_path(index):
-    return os.path.join(INDEXDIR, index)
+    return safe_join(INDEXDIR, index)
 
 
 @scope_blueprint.route('/obj/<path:obj_path>')
 def get_object_src_http(obj_path):
     path = _get_obj_absolute_path(obj_path)
diff --git a/opendiamond/dataretriever/video_store.py b/opendiamond/dataretriever/video_store.py
index 7d36ec6c..00dfe1aa 100644
--- a/opendiamond/dataretriever/video_store.py
+++ b/opendiamond/dataretriever/video_store.py
@@ -19,10 +19,11 @@ import sys
 from math import ceil
 
 from flask import Blueprint, Response, request, stream_with_context, url_for
 from opendiamond.dataretriever.util import DiamondTextAttr
 from werkzeug.datastructures import Headers
+from werkzeug.security import safe_join
 
 # IMPORTANT: requires ffmpeg >= 3.3. Lower versions produce incorrect clipping.
 
 BASEURL = 'video'
 STYLE = False
@@ -120,15 +121,15 @@ def _get_object_element(start, span, video):
         url_for('.get_object_id', start=start, span=span, video=video),
         url_for('.get_object', start=start, span=span, video=video))
 
 
 def _get_obj_absolute_path(obj_path):
-    return os.path.join(DATAROOT, obj_path)
+    return safe_join(DATAROOT, obj_path)
 
 
 def _get_index_absolute_path(index):
-    return os.path.join(INDEXDIR, index)
+    return safe_join(INDEXDIR, index)
 
 
 def _ffprobe(video_path):
     cmd_l = ['ffprobe', '-v', 'quiet', '-print_format', 'json',
                 '-show_format', video_path]
diff --git a/opendiamond/dataretriever/yfcc100m_mysql_store.py b/opendiamond/dataretriever/yfcc100m_mysql_store.py
index 8ee4634c..c8ac1b24 100644
--- a/opendiamond/dataretriever/yfcc100m_mysql_store.py
+++ b/opendiamond/dataretriever/yfcc100m_mysql_store.py
@@ -27,10 +27,11 @@ import os
 from flask import Blueprint, url_for, Response, \
     stream_with_context, abort, jsonify, send_file
 import logging
 import mysql.connector
 from werkzeug.datastructures import Headers
+from werkzeug.security import safe_join
 from xml.sax.saxutils import quoteattr
 
 BASEURL = 'yfcc100m_mysql'
 STYLE = False
 LOCAL_OBJ_URI = True  # if true, return local path, otherwise http.
@@ -122,21 +123,21 @@ def get_object_id(dataset, seq_no):
                     "200 OK",
                     headers=headers)
 
 @scope_blueprint.route('/obj/<dataset>/<path:rel_path>')
 def get_object_src_http(dataset, rel_path):
-    path = _get_obj_abosolute_path(dataset, rel_path)
+    path = _get_obj_absolute_path(dataset, rel_path)
     response = send_file(path,
                          cache_timeout=datetime.timedelta(
                              days=365).total_seconds(),
                          add_etags=True,
                          conditional=True)
     return response
 
 
-def _get_obj_abosolute_path(dataset, rel_path):
-    return os.path.join(DATAROOT, dataset, rel_path)
+def _get_obj_absolute_path(dataset, rel_path):
+    return safe_join(DATAROOT, dataset, rel_path)
 
 
 def _get_object_element(dataset, seq_no, rel_path, download_link):
     """If rel_path and download_link are not None, we are called from scope.
     Otherwise we are called from ID and need to run SQL query to fetch these attrs."""
@@ -160,11 +161,11 @@ def _get_object_element(dataset, seq_no, rel_path, download_link):
             return None
 
         rel_path, download_link = row[0], row[1]
 
     if LOCAL_OBJ_URI:
-        src_uri = 'file://' + os.path.join(DATAROOT, dataset, rel_path)
+        src_uri = 'file://' + _get_obj_absolute_path(dataset, rel_path)
     else:
         src_uri = url_for('.get_object_src_http', dataset=dataset, rel_path=rel_path)
 
     return '<object id={} src={} hyperfind.external-link={} />' \
         .format(

Vulnerable code: from werkzeug.utils import safe_join
    return os.path.join(DATAROOT, obj_path)
    return os.path.join(INDEXDIR, index)
    return os.path.join(DATAROOT, obj_path)
    return os.path.join(INDEXDIR, index)
    return os.path.join(DATAROOT, obj_path)
    return os.path.join(INDEXDIR, index)
    return os.path.join(DATAROOT, obj_path)
    return os.path.join(INDEXDIR, index)
    path = _get_obj_abosolute_path(dataset, rel_path)
def _get_obj_abosolute_path(dataset, rel_path):
    return os.path.join(DATAROOT, dataset, rel_path)
        src_uri = 'file://' + os.path.join(DATAROOT, dataset, rel_path)
Fixed code: from werkzeug.security import safe_join
from werkzeug.security import safe_join
    return safe_join(DATAROOT, obj_path)
    return safe_join(INDEXDIR, index)
from werkzeug.security import safe_join
    return safe_join(DATAROOT, obj_path)
    return safe_join(INDEXDIR, index)
from werkzeug.security import safe_join

    return safe_join(DATAROOT, obj_path)
    return safe_join(INDEXDIR, index)
from werkzeug.security import safe_join
    return safe_join(DATAROOT, obj_path)
    return safe_join(INDEXDIR, index)
from werkzeug.security import safe_join
    path = _get_obj_absolute_path(dataset, rel_path)
def _get_obj_absolute_path(dataset, rel_path):
    return safe_join(DATAROOT, dataset, rel_path)
        src_uri = 'file://' + _get_obj_absolute_path(dataset, rel_path)