DEV Community

Zeyrian Faris
Zeyrian Faris

Posted on

I Found a Source Code Disclosure Bug in My Own Flask App With Gobuster

Context

I'm a first-year Cybersecurity and Digital Forensics student, currently working through TryHackMe's Jr Pentester path. While practicing directory enumeration with gobuster, I had an idea: rather than only running it against lab targets, why not point it at something I actually built and care about?

That something is fourpointo. It Flask app I'm developing that generates AI task lists and rubric breakdowns from uploaded assignment PDFs, self-hosted on a Dell server and deployed via Cloudflare Tunnel.

I wasn't expecting to find anything. I was mostly testing whether I understood the tool. I ended up finding a real, textbook misconfiguration and figured it was worth documenting properly, both as a learning exercise and as a small case study in why this category of bug is so common.

The Setup

To do this safely, I didn't touch the live production app (fourpointo has real registered users and testing against it directly wasn't worth the risk). Instead, I cloned the public GitHub repo and ran a local dev copy on an isolated Kali VM, hitting localhost only.

I then deliberately introduced a misconfiguration I wanted to study: I copied database.py, one of the app's core server-side source files, into the static/ directory.

Why this matters: in Flask, static/ is auto-served by the framework with no route required. Anything dropped in there, intentionally or by accident, is publicly reachable. It's meant to hold CSS, JS, images: client-facing assets. Files in templates/, by contrast, are not directly reachable; they only render through actual app routes. Mixing a server-side source file into static/ blurs a trust boundary that should be kept firm.

This is a realistic mistake. It's easy to imagine a developer copying a file for a quick test, or a build script misplacing something, and forgetting to remove it before deploying.

Discovery

With the dev server running locally, I ran:

gobuster dir -u http://localhost:5000/static -w /usr/share/wordlists/dirb/common.txt -x html,htm,php,py,js,json,txt,xml,bak,old,zip,sql,env,config,yml -t 50
Enter fullscreen mode Exit fullscreen mode

Output:

database.py     (Status: 200) [Size: 7739]
script.js        (Status: 200) [Size: 8746]
Progress: 73808 / 73808 (100.00%)
Finished
Enter fullscreen mode Exit fullscreen mode

script.js showing up is expected as it is a legitimate static asset. database.py showing up alongside it is not.

Confirming the Finding

A 200 status from gobuster tells you a path exists and returned content but it doesn't tell you what that content is. To confirm actual impact, I pulled the file directly:

curl http://localhost:5000/static/database.py
Enter fullscreen mode Exit fullscreen mode

This returned the full, raw source of database.py. No authentication, no warning, just plain text served like any other static asset.

The file contained:

  • Full database schema (table definitions for projects, tasks, and others, including column names and relationships)
  • Application logic for handling user data, including how passwords are hashed before being written to the database
  • Enough structural detail to understand exactly how the app's data model and core functions work, without needing to guess

To be clear: no real user data, hardcoded secrets, or API keys were exposed in this case. Fourpointo correctly keeps those in environment variables, not in source. But that's somewhat beside the point. The schema and logic disclosure alone is a meaningful leak on its own, and in a less careful codebase, this exact misconfiguration could just as easily have exposed live credentials.

Why This Matters

Source code disclosure is generally underrated as a finding because it doesn't look as dramatic as, say, SQL injection or an exposed admin panel. But it directly enables both:

  1. Targeted attacks: knowing exact table names, column names, and queries removes the guesswork an attacker would otherwise need for SQL injection, IDOR, or logic-abuse attempts.
  2. Reconnaissance for the next bug: reading real application logic often reveals other weaknesses (missing validation, unsafe assumptions, auth logic quirks) that would be far harder to find through black-box testing alone.

It's also a good demonstration of why directory brute-forcing matters in a real pentest. In practice you rarely know a target's folder structure in advance. You fingerprint the stack (Flask, in this case, identifiable through default error pages, cookie naming, etc.), pick a wordlist suited to that stack, and brute-force from there. Knowing Flask serves static/ by default is exactly the kind of framework-specific knowledge that turns a generic scan into a targeted one.

Remediation

The fix is straightforward: keep server-side source code out of any directory the web server treats as public:

  • Never place .py (or other server-side source) files inside static/
  • At the web server/proxy level, explicitly deny serving source file extensions as a defense-in-depth measure, e.g. for Nginx:
location ~* \.py$ {
      deny all;
  }
Enter fullscreen mode Exit fullscreen mode
  • Periodically audit what's actually inside static/ as it's easy for stray files to accumulate over time without anyone noticing

Takeaway

The lesson that stuck with me most: a directory's purpose and a directory's actual contents can quietly drift apart, and nothing forces you to notice until someone runs a scan. Treating static/ (or any auto-served directory) as inherently safe just because of what it's meant to hold, rather than auditing what's actually in there, is exactly the kind of assumption that creates this category of bug.

Testing this against my own app, rather than only against lab targets, was a useful exercise precisely because the stakes felt real even at a small scale: this is a live app with actual users, and the same gap could plausibly have existed in production without me thinking to check.

Top comments (0)