CVE-2022-23530: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: GuardDog is a CLI tool to identify malicious PyPI packages. Versions prior to v0.1.8 are vulnerable to arbitrary file write when scanning a specially-crafted remote PyPI package. Extracting files using shutil.unpack_archive() from a potentially malicious tarball without validating that the destination file path is within the intended destination directory can cause files outside the destination directory to be overwritten.  This issue is patched in version 0.1.8. Potential workarounds include using a safer module, like zipfile, and validating the location of the extracted files and discarding those with malicious paths.
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: 2022-12-16T23:15Z
Repository: https://github.com/DataDog/guarddog
Language: YAML
Files changed: .github/semgrep-rules/insecure-unpack-archive.yml, .github/workflows/semgrep.yml, guarddog/scanners/package_scanner.py
Commit date: 2022-12-05 17:08:54 +0100
Commit message: Securely extract PyPI .tar.gz archives (#102)

* Add Semgrep rule and run custom Semgrep rules in CI for SAST

* Securely extract remote .tar.gz files
Diff: diff --git a/.github/semgrep-rules/insecure-unpack-archive.yml b/.github/semgrep-rules/insecure-unpack-archive.yml
new file mode 100644
index 0000000..d40f645
--- /dev/null
+++ b/.github/semgrep-rules/insecure-unpack-archive.yml
@@ -0,0 +1,21 @@
+rules:
+  - id: insecure-shutil-unpack-archive-use
+    message: The Python 'shutil' shutil.extract_archive is vulnerable to
+      arbitrary file overwrites
+    languages:
+      - python
+    severity: ERROR
+    metadata:
+      category: security
+      technology:
+        - python
+      owasp:
+        - A06:2017 - Security Misconfiguration
+        - A05:2021 - Security Misconfiguration
+      cwe:
+        - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory
+          ('Path Traversal')"
+      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
+    pattern-either:
+      - pattern: |
+          shutil.unpack_archive(...)
\ No newline at end of file
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index e16e8ba..80320c4 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -24,11 +24,19 @@ jobs:
 
     steps:
       - uses: actions/checkout@v3
 
       - run: semgrep --config auto --sarif --output semgrep.sarif ./guarddog
+      - run: semgrep --config .github/semgrep-rules --sarif --output semgrep-custom.sarif ./guarddog
 
       - name: Upload SARIF file for GitHub Advanced Security Dashboard
         uses: github/codeql-action/upload-sarif@v2
         with:
+          category: semgrep-builtin
           sarif_file: semgrep.sarif
-        if: always()
+
+      - name: Upload SARIF file for custom Semgrep rules for GitHub Advanced Security Dashboard
+        uses: github/codeql-action/upload-sarif@v2
+        with:
+          category: semgrep-custom
+          sarif_file: semgrep-custom.sarif
+          
diff --git a/guarddog/scanners/package_scanner.py b/guarddog/scanners/package_scanner.py
index 79f216f..0647c9a 100644
--- a/guarddog/scanners/package_scanner.py
+++ b/guarddog/scanners/package_scanner.py
@@ -1,9 +1,8 @@
 import json
 import os
-import shutil
-import tarsafe  # type: ignore
+import tarsafe  # type:ignore
 import tempfile
 import requests
 
 from guarddog.analyzer.analyzer import Analyzer
 from guarddog.scanners.scanner import Scanner
@@ -158,7 +157,10 @@ class PackageScanner(Scanner):
         response = requests.get(url, stream=True)
 
         with open(zippath, "wb") as f:
             f.write(response.raw.read())
 
-        shutil.unpack_archive(zippath, unzippedpath)
-        os.remove(zippath)
+        if zippath.endswith('.tar.gz'):
+            tarsafe.open(zippath).extractall(unzippedpath)
+            os.remove(zippath)
+        else:
+            raise ValueError("unsupported archive extension: " + zippath)

Vulnerable code:         if: always()
import shutil
import tarsafe  # type: ignore
        shutil.unpack_archive(zippath, unzippedpath)
        os.remove(zippath)
Fixed code: rules:
  - id: insecure-shutil-unpack-archive-use
    message: The Python 'shutil' shutil.extract_archive is vulnerable to
      arbitrary file overwrites
    languages:
      - python
    severity: ERROR
    metadata:
      category: security
      technology:
        - python
      owasp:
        - A06:2017 - Security Misconfiguration
        - A05:2021 - Security Misconfiguration
      cwe:
        - "CWE-22: Improper Limitation of a Pathname to a Restricted Directory
          ('Path Traversal')"
      license: Commons Clause License Condition v1.0[LGPL-2.1-only]
    pattern-either:
      - pattern: |
          shutil.unpack_archive(...)
      - run: semgrep --config .github/semgrep-rules --sarif --output semgrep-custom.sarif ./guarddog
          category: semgrep-builtin

      - name: Upload SARIF file for custom Semgrep rules for GitHub Advanced Security Dashboard
        uses: github/codeql-action/upload-sarif@v2
        with:
          category: semgrep-custom
          sarif_file: semgrep-custom.sarif
          
import tarsafe  # type:ignore
        if zippath.endswith('.tar.gz'):
            tarsafe.open(zippath).extractall(unzippedpath)
            os.remove(zippath)
        else:
            raise ValueError("unsupported archive extension: " + zippath)