From 9ee308dcd77e57e66eb3d91c1787c698f5f0c36a Mon Sep 17 00:00:00 2001 From: "semgrep.dev on behalf of @nicholas.harvey@semgrep.com" Date: Fri, 26 Jun 2026 18:06:36 +0000 Subject: [PATCH 01/11] Add Semgrep CI --- .github/workflows/semgrep.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .github/workflows/semgrep.yml diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml new file mode 100644 index 00000000..ef2f1e5b --- /dev/null +++ b/.github/workflows/semgrep.yml @@ -0,0 +1,26 @@ +on: + workflow_dispatch: {} + pull_request: {} + push: + branches: + - main + - master + paths: + - .github/workflows/semgrep.yml + schedule: + # random HH:MM to avoid a load spike on GitHub Actions at 00:00 + - cron: 6 9 * * * +name: Semgrep +jobs: + semgrep: + name: semgrep/ci + runs-on: ubuntu-latest + permissions: + contents: read + env: + SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} + container: + image: semgrep/semgrep + steps: + - uses: actions/checkout@v6 + - run: semgrep ci From 645aedd6bdbd73cada7543172dd7d1059e253eda Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Tue, 30 Jun 2026 14:27:30 -0700 Subject: [PATCH 02/11] Test vuln --- test-vuln.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 test-vuln.py diff --git a/test-vuln.py b/test-vuln.py new file mode 100644 index 00000000..1123d309 --- /dev/null +++ b/test-vuln.py @@ -0,0 +1,23 @@ +import os +import flask + +app = flask.Flask(__name__) + + +@app.route("/route_param/") +def route_param(route_param): + + # ruleid:dangerous-os-exec + os.execl("/bin/bash", "/bin/bash", "-c", route_param) + + return "oops!" + + +# Flask true negatives +@app.route("/route_param/") +def route_param2(route_param): + + # ok:dangerous-os-exec + os.execl("static") + + return "ok!" \ No newline at end of file From ebfb460c91463dffc23639b129ef36104d5c60f0 Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Tue, 30 Jun 2026 14:32:28 -0700 Subject: [PATCH 03/11] Testing --- test-vuln.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/test-vuln.py b/test-vuln.py index 1123d309..e69de29b 100644 --- a/test-vuln.py +++ b/test-vuln.py @@ -1,23 +0,0 @@ -import os -import flask - -app = flask.Flask(__name__) - - -@app.route("/route_param/") -def route_param(route_param): - - # ruleid:dangerous-os-exec - os.execl("/bin/bash", "/bin/bash", "-c", route_param) - - return "oops!" - - -# Flask true negatives -@app.route("/route_param/") -def route_param2(route_param): - - # ok:dangerous-os-exec - os.execl("static") - - return "ok!" \ No newline at end of file From 5acd2a0242cbec9894186d92dcc971c2e80ac77c Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Tue, 30 Jun 2026 17:32:09 -0700 Subject: [PATCH 04/11] More testing --- test-vuln.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test-vuln.py b/test-vuln.py index e69de29b..1123d309 100644 --- a/test-vuln.py +++ b/test-vuln.py @@ -0,0 +1,23 @@ +import os +import flask + +app = flask.Flask(__name__) + + +@app.route("/route_param/") +def route_param(route_param): + + # ruleid:dangerous-os-exec + os.execl("/bin/bash", "/bin/bash", "-c", route_param) + + return "oops!" + + +# Flask true negatives +@app.route("/route_param/") +def route_param2(route_param): + + # ok:dangerous-os-exec + os.execl("static") + + return "ok!" \ No newline at end of file From 727e1585291b0bb543604d2a558e9484dcd02947 Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Tue, 30 Jun 2026 17:33:47 -0700 Subject: [PATCH 05/11] Testing --- path-traversal-chained-test.py | 34 ++++++++++++++++++++++++++++++ path-traversal-chained.yaml | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 path-traversal-chained-test.py create mode 100644 path-traversal-chained.yaml diff --git a/path-traversal-chained-test.py b/path-traversal-chained-test.py new file mode 100644 index 00000000..e465388a --- /dev/null +++ b/path-traversal-chained-test.py @@ -0,0 +1,34 @@ +import os + +UPLOAD_DIR = "/var/uploads" +SAFE_DIR = "/var/safe" + +# ruleid: path-traversal-open-os-path-join +# SHOULD MATCH — user_filename flows directly into os.path.join then open() +def bad_file_read(user_filename): + path = os.path.join(UPLOAD_DIR, user_filename) + with open(path, 'r') as f: + return f.read() + +# ruleid: path-traversal-open-os-path-join +# SHOULD MATCH — inline: os.path.join passed directly into open() in one expression +def bad_inline(user_filename): + with open(os.path.join(UPLOAD_DIR, user_filename), 'rb') as f: + return f.read() + +# ok: path-traversal-open-os-path-join +# SHOULD NOT MATCH — hardcoded filename, no user input +def safe_hardcoded(): + path = os.path.join(UPLOAD_DIR, "config.json") + with open(path, 'r') as f: + return f.read() + +# ok: path-traversal-open-os-path-join +# SHOULD NOT MATCH — path is validated before open() +def safe_validated(user_filename): + joined = os.path.join(UPLOAD_DIR, user_filename) + safe = os.path.abspath(joined) + if not safe.startswith(os.path.abspath(UPLOAD_DIR)): + raise ValueError("Path traversal detected") + with open(safe, 'r') as f: + return f.read() diff --git a/path-traversal-chained.yaml b/path-traversal-chained.yaml new file mode 100644 index 00000000..ffc54ce7 --- /dev/null +++ b/path-traversal-chained.yaml @@ -0,0 +1,38 @@ +rules: + - id: path-traversal-open-os-path-join-inline + pattern: open(os.path.join($BASE, $USER_INPUT), ...) + pattern-not: open(os.path.join("...", "..."), ...) + message: > + Path traversal: os.path.join() result passed directly to open() without + path validation. An attacker controlling the second argument can escape + the base directory using '../' sequences. Fix: resolve the path with + os.path.abspath() and assert it starts with os.path.abspath($BASE). + languages: [python] + severity: ERROR + metadata: + cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory" + owasp: "A01:2021 - Broken Access Control" + confidence: HIGH + category: security + + - id: path-traversal-open-os-path-join-two-step + patterns: + - pattern: | + $PATH = os.path.join($BASE, $USER_INPUT) + open($PATH, ...) + - pattern-not: | + $PATH = os.path.join("...", "...") + open($PATH, ...) + message: > + Path traversal: a path built with os.path.join() is passed to open() + without validation in between. An attacker controlling the second + argument of os.path.join() can escape the base directory. Fix: call + os.path.abspath() on $PATH and verify it starts with + os.path.abspath($BASE) before calling open(). + languages: [python] + severity: ERROR + metadata: + cwe: "CWE-22: Improper Limitation of a Pathname to a Restricted Directory" + owasp: "A01:2021 - Broken Access Control" + confidence: HIGH + category: security From 1880098f8cf20c6145b83e9eed90b7c18cae5496 Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Tue, 30 Jun 2026 17:45:40 -0700 Subject: [PATCH 06/11] Testing --- test-vuln.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-vuln.py b/test-vuln.py index 1123d309..51a50c17 100644 --- a/test-vuln.py +++ b/test-vuln.py @@ -20,4 +20,4 @@ def route_param2(route_param): # ok:dangerous-os-exec os.execl("static") - return "ok!" \ No newline at end of file + return "Ok!" \ No newline at end of file From 9f42e25287f9ae26911917b6f70668436ae304fd Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Mon, 6 Jul 2026 18:48:12 -0700 Subject: [PATCH 07/11] Testing .semgrepignore for dependencies --- .semgrepignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .semgrepignore diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..d54bfb55 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1 @@ +requirements.txt \ No newline at end of file From 0b28a1d1b8dd08dbff0724cc859f78b67a6dbb9a Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Mon, 6 Jul 2026 19:00:38 -0700 Subject: [PATCH 08/11] Removing ignored file --- .semgrepignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.semgrepignore b/.semgrepignore index d54bfb55..e69de29b 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -1 +0,0 @@ -requirements.txt \ No newline at end of file From ab5f3950a7d634c5a83306b2e99da8127a385283 Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Wed, 22 Jul 2026 11:05:09 -0700 Subject: [PATCH 09/11] Testing license detection --- .semgrepignore | 1 + requirements.txt | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.semgrepignore b/.semgrepignore index e69de29b..94656643 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 96714824..bb433ce9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ +ansible==2.16.0 certifi==2021.10.8 +chardet==3.0.4 charset-normalizer==2.0.12 click==8.0.1 Flask==2.0.1 From b8f514ec962be1725120d43a501a0bc0100fdab0 Mon Sep 17 00:00:00 2001 From: Nicholas Harvey Date: Wed, 22 Jul 2026 11:23:32 -0700 Subject: [PATCH 10/11] Updating Ansible --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bb433ce9..6818611a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ansible==2.16.0 +ansible-core==2.16.0 certifi==2021.10.8 chardet==3.0.4 charset-normalizer==2.0.12 From a1b23ca2c3557f75dc82dd7df0d5111d0acd934a Mon Sep 17 00:00:00 2001 From: narvey-semgrep Date: Tue, 1 Sep 2026 09:09:50 -0700 Subject: [PATCH 11/11] Testing --- test-vuln.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test-vuln.py b/test-vuln.py index 51a50c17..753671cd 100644 --- a/test-vuln.py +++ b/test-vuln.py @@ -1,23 +1,22 @@ -import os -import flask +import sh -app = flask.Flask(__name__) +long = os.environ.get("LONG", "") +# ruleid: string-concat +sh.ls("-a" + long) +# ok: string-concat +sh.ls("-al") -@app.route("/route_param/") -def route_param(route_param): +# ok: string-concat +sh.semgrep("--config", "https://semgrep.dev/p/r2c-CI") - # ruleid:dangerous-os-exec - os.execl("/bin/bash", "/bin/bash", "-c", route_param) +confurl = os.environ.get("SEMGREP_CONFIG_URL", "") +# ruleid: string-concat +sh.semgrep("--config {}".format(confurl)) - return "oops!" +# ruleid: string-concat +sh.semgrep(f"--config {confurl}") - -# Flask true negatives -@app.route("/route_param/") -def route_param2(route_param): - - # ok:dangerous-os-exec - os.execl("static") - - return "Ok!" \ No newline at end of file +# ok: string-concat +args = ["--config", confurl] +sh.semgrep(*args)