-
Notifications
You must be signed in to change notification settings - Fork 4
test(fixture): intentionally vulnerable app for advisory-scanning pipeline [DO NOT MERGE] #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+35
to
+36
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Dependabot generates repository alerts from manifests on the default branch, not from manifests added only on a pull-request branch. Therefore, opening this fixture as the promised do-not-merge PR will produce the CodeQL results but not the Dependabot alerts needed for the advisory-to-commit experiment. Use a dependency-review/advisory scan that runs against the PR contents, or test the Dependabot portion in a disposable repository where the fixture can become the default branch. Useful? React with 👍 / 👎. |
||
| all of it, giving the introducing-commit / fixing-commit pair used to exercise | ||
| advisory-to-commit correlation. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a source distribution is built, Hatch includes tracked repository files by default because the project only restricts the wheel target and has no sdist exclusion. As a result, release builds can contain this executable vulnerable app and its deliberately vulnerable manifests despite the stated packaging boundary. Add an explicit Hatch sdist exclusion for
fixtures/vulnerable-app/.Useful? React with 👍 / 👎.