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 }} 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) 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