From 130afd3571be7bdc24f42c411f78713f32d561db Mon Sep 17 00:00:00 2001 From: Matheus Theodoro Date: Sat, 22 Aug 2026 11:24:08 -0300 Subject: [PATCH 1/3] test(fixture): add dependency manifests pinned to advisory-affected versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intentionally vulnerable test fixture for the advisory-scanning pipeline. Pins PyYAML 5.3.1, Jinja2 2.10, requests 2.19.1, urllib3 1.24.1, Flask 0.12.2, Pillow 8.1.0 and cryptography 3.3.2 — each has a published GHSA — plus lodash/minimist/axios/handlebars on the npm side. Not installed by the project and not on any runtime path. --- fixtures/vulnerable-app/package.json | 13 +++++++++++++ fixtures/vulnerable-app/requirements.txt | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 fixtures/vulnerable-app/package.json create mode 100644 fixtures/vulnerable-app/requirements.txt diff --git a/fixtures/vulnerable-app/package.json b/fixtures/vulnerable-app/package.json new file mode 100644 index 0000000..0d30017 --- /dev/null +++ b/fixtures/vulnerable-app/package.json @@ -0,0 +1,13 @@ +{ + "name": "vulnerable-app-fixture", + "version": "0.0.0", + "private": true, + "description": "INTENTIONALLY VULNERABLE npm manifest. Test fixture only - do not install or deploy.", + "license": "UNLICENSED", + "dependencies": { + "lodash": "4.17.11", + "minimist": "1.2.0", + "axios": "0.21.0", + "handlebars": "4.0.13" + } +} diff --git a/fixtures/vulnerable-app/requirements.txt b/fixtures/vulnerable-app/requirements.txt new file mode 100644 index 0000000..2f2af86 --- /dev/null +++ b/fixtures/vulnerable-app/requirements.txt @@ -0,0 +1,23 @@ +# ⚠️ INTENTIONALLY VULNERABLE — test fixture for advisory-scanning pipeline. +# Every pin below has a published GitHub Security Advisory. Do not copy. + +# GHSA-8q59-q68h-6hv4 / CVE-2020-14343 — arbitrary code execution via yaml.full_load +PyYAML==5.3.1 + +# GHSA-462w-v97r-4m45 / CVE-2019-10906 — sandbox escape in the Jinja2 sandbox +Jinja2==2.10 + +# GHSA-x84v-xcm2-53pg / CVE-2018-18074 — Authorization header leaked across redirects +requests==2.19.1 + +# GHSA-mh33-7rrq-662w / CVE-2019-11324 — CA cert handling regression +urllib3==1.24.1 + +# GHSA-5wv5-4vpf-pj6m / CVE-2018-1000656 — denial of service on malformed JSON +Flask==0.12.2 + +# GHSA-8vj2-vgrf-5rv6 / CVE-2021-25287 — out-of-bounds read in the JPEG2000 decoder +Pillow==8.1.0 + +# GHSA-x4qr-2fvf-3mr5 / CVE-2023-23931 — memory corruption via Cipher.update_into +cryptography==3.3.2 From 138b29767d83fd1612059a3fbb2baa8551ac55e4 Mon Sep 17 00:00:00 2001 From: Matheus Theodoro Date: Sat, 22 Aug 2026 11:24:08 -0300 Subject: [PATCH 2/3] test(fixture): add intentionally vulnerable Flask app Plants SQL injection, OS command injection, unsafe yaml.load and pickle deserialization, SSTI, path traversal, SSRF, disabled TLS verification, MD5 password hashing and hardcoded credentials so code scanning has deterministic findings to report. Each weakness is annotated inline with the secure approach that the real implementation would use. --- fixtures/vulnerable-app/README.md | 38 ++++++++ fixtures/vulnerable-app/app.py | 151 ++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 fixtures/vulnerable-app/README.md create mode 100644 fixtures/vulnerable-app/app.py diff --git a/fixtures/vulnerable-app/README.md b/fixtures/vulnerable-app/README.md new file mode 100644 index 0000000..6dd08a4 --- /dev/null +++ b/fixtures/vulnerable-app/README.md @@ -0,0 +1,38 @@ +# ⚠️ INTENTIONALLY VULNERABLE FIXTURE — DO NOT DEPLOY + +This directory exists **only** as a test fixture for a security-scanning pipeline +(GitHub Advisory Database / Dependabot / CodeQL / secret scanning). + +Every file here is deliberately insecure. It is not imported by `src/redthread`, +is not on any runtime path, and must never be packaged, deployed, or executed +outside a throwaway sandbox. + +## What is planted here + +### 1. Dependency advisories (GHSA / Dependabot) +`requirements.txt` pins packages to versions with published advisories: + +| Package | Pinned | Advisory | CVE | +|---|---|---|---| +| PyYAML | 5.3.1 | GHSA-8q59-q68h-6hv4 | CVE-2020-14343 | +| Jinja2 | 2.10 | GHSA-462w-v97r-4m45 | CVE-2019-10906 | +| requests | 2.19.1 | GHSA-x84v-xcm2-53pg | CVE-2018-18074 | +| urllib3 | 1.24.1 | GHSA-mh33-7rrq-662w | CVE-2019-11324 | +| Flask | 0.12.2 | GHSA-5wv5-4vpf-pj6m | CVE-2018-1000656 | +| Pillow | 8.1.0 | GHSA-8vj2-vgrf-5rv6 | CVE-2021-25287 | +| cryptography | 3.3.2 | GHSA-x4qr-2fvf-3mr5 | CVE-2023-23931 | + +`package.json` does the same for the npm ecosystem (lodash prototype pollution, +minimist argument injection). + +### 2. Source-level weaknesses (CodeQL / Semgrep) +`app.py` contains SQL injection, OS command injection, unsafe YAML and pickle +deserialization, SSTI, path traversal, SSRF, a weak hash, disabled TLS +verification, and a hardcoded credential. Each is annotated inline with the +secure approach that would have been used in real code. + +## Expected pipeline behaviour +Opening this branch as a PR should produce Dependabot alerts for the manifests +above and code-scanning alerts for `app.py`. The follow-up "fix" PR reverses +all of it, giving the introducing-commit / fixing-commit pair used to exercise +advisory-to-commit correlation. diff --git a/fixtures/vulnerable-app/app.py b/fixtures/vulnerable-app/app.py new file mode 100644 index 0000000..895361c --- /dev/null +++ b/fixtures/vulnerable-app/app.py @@ -0,0 +1,151 @@ +"""INTENTIONALLY VULNERABLE Flask app — security-pipeline test fixture. + +Not imported by ``src/redthread``. Not on any runtime path. Never deploy this. + +Each handler below plants one well-known weakness so that code scanning has +something deterministic to flag. Every one carries a ``SECURE APPROACH`` note +describing what the real implementation would do. +""" + +import hashlib +import os +import pickle +import sqlite3 +import subprocess + +import requests +import yaml +from flask import Flask, request, send_file +from jinja2 import Template + +app = Flask(__name__) + +# DANGEROUS: hardcoded credentials committed to source control. +# SECURE APPROACH: load from the environment or a secret manager (AWS Secrets +# Manager / Vault) at startup, keep the value out of git entirely, and add a +# pre-commit secret scanner so a literal like this can never be committed. +DB_PASSWORD = "sup3rs3cr3t-admin-password" +GITHUB_TOKEN = "ghp_0123456789abcdefghijklmnopqrstuvwxyzAB" +AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" + +# DANGEROUS: debug mode also enables the Werkzeug interactive debugger, which is +# a remote code execution primitive if it is ever reachable off-localhost. +# SECURE APPROACH: drive this from an explicit env flag that defaults to off, +# and never let the production config path set it. +app.config["DEBUG"] = True + + +@app.route("/user") +def get_user(): + """DANGEROUS: SQL injection — user input is concatenated into the query. + + SECURE APPROACH: use a parameterised query + (``cur.execute("SELECT ... WHERE username = ?", (username,))``) so the + driver binds the value instead of splicing it into SQL text, or go through + an ORM layer that does this by construction. + """ + username = request.args.get("username", "") + conn = sqlite3.connect("users.db") + cur = conn.cursor() + query = f"SELECT id, email FROM users WHERE username = '{username}'" # noqa: S608 + cur.execute(query) + return {"rows": cur.fetchall()} + + +@app.route("/ping") +def ping(): + """DANGEROUS: OS command injection — ``shell=True`` on attacker input. + + SECURE APPROACH: never build a shell string. Pass an argument list with + ``shell=False`` (``subprocess.run(["ping", "-c", "1", host])``) and + validate ``host`` against a strict allowlist or an IP/hostname regex first. + """ + host = request.args.get("host", "127.0.0.1") + output = subprocess.check_output(f"ping -c 1 {host}", shell=True) + return {"output": output.decode()} + + +@app.route("/config", methods=["POST"]) +def load_config(): + """DANGEROUS: ``yaml.load`` with the default loader executes arbitrary + Python tags (this is exactly CVE-2020-14343 in the pinned PyYAML). + + SECURE APPROACH: use ``yaml.safe_load``, which refuses object-construction + tags, and validate the resulting dict against a schema (pydantic) before + using any of it. + """ + return {"config": yaml.load(request.data)} + + +@app.route("/session", methods=["POST"]) +def restore_session(): + """DANGEROUS: unpickling untrusted bytes is arbitrary code execution. + + SECURE APPROACH: never use pickle as a wire format. Serialise sessions as + JSON, and if the payload must be trusted across a boundary, sign it + (HMAC-SHA256) and verify the signature before parsing. + """ + return {"session": str(pickle.loads(request.data))} + + +@app.route("/render") +def render(): + """DANGEROUS: server-side template injection — user input compiled as a + Jinja2 template, which reaches Python objects and then the interpreter. + + SECURE APPROACH: treat user input as *data*, never as template source: + ``Template(FIXED_TEMPLATE).render(name=user_input)``. Autoescaping on, and + no user-controlled template text ever reaches the compiler. + """ + template = request.args.get("template", "hello") + return Template(template).render() + + +@app.route("/download") +def download(): + """DANGEROUS: path traversal — ``../../etc/passwd`` escapes the base dir. + + SECURE APPROACH: resolve the joined path and assert it is still inside the + base directory (``os.path.commonpath``/``Path.resolve().is_relative_to``), + or better, look the file up by an opaque ID in a database instead of + letting the client supply any part of a filesystem path. + """ + filename = request.args.get("file", "readme.txt") + return send_file(os.path.join("/var/app/files", filename)) + + +@app.route("/fetch") +def fetch(): + """DANGEROUS: SSRF plus disabled TLS verification. + + ``verify=False`` turns every HTTPS call into a trivially interceptable + plaintext-equivalent channel, and the unvalidated URL lets a caller reach + internal services and cloud metadata endpoints (169.254.169.254). + + SECURE APPROACH: keep ``verify=True`` (fix the trust store instead of + disabling the check), and put the URL through an allowlist of schemes and + hosts, resolving DNS first and rejecting private/link-local address ranges + before the request goes out. + """ + url = request.args.get("url", "") + resp = requests.get(url, verify=False, timeout=10) + return {"body": resp.text[:500]} + + +def hash_password(password: str) -> str: + """DANGEROUS: MD5, unsalted, for password storage. + + SECURE APPROACH: use a memory-hard password hash designed for this — + argon2id (or bcrypt/scrypt) with a per-user salt and tuned cost parameters. + MD5 is both broken for collision resistance and far too fast to resist + offline cracking. + """ + return hashlib.md5(password.encode()).hexdigest() # noqa: S324 + + +if __name__ == "__main__": + # DANGEROUS: binds every interface with the interactive debugger enabled. + # SECURE APPROACH: bind 127.0.0.1 behind a reverse proxy, debug off, and + # serve through a production WSGI server (gunicorn/uvicorn) rather than + # Werkzeug's development server. + app.run(host="0.0.0.0", port=5000, debug=True) From 0ad5900a28ee32171e43f3c40e7844038471c7a5 Mon Sep 17 00:00:00 2001 From: Matheus Theodoro Date: Sat, 22 Aug 2026 11:24:08 -0300 Subject: [PATCH 3/3] ci: add CodeQL analysis workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Runs on push/PR to main so the vulnerable fixture produces code-scanning alerts. Analysis only — it never opens pull requests. --- .github/workflows/codeql.yml | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..2b089a6 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,39 @@ +name: CodeQL + +# Added so the intentionally vulnerable fixture under fixtures/vulnerable-app/ +# produces real code-scanning alerts on pull requests. Safe to keep: it only +# analyses code, it never opens PRs. + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: "0 6 * * 1" + +permissions: + contents: read + security-events: write + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + language: [python] + steps: + - uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + queries: security-extended + + - name: Perform CodeQL analysis + uses: github/codeql-action/analyze@v3 + with: + category: /language:${{ matrix.language }}