Two criticals before launch: running a pentest on your own app

may 29 2026 · 4 min read

The Overtime Calculator handles sensitive personal financial data. Before pointing a public DNS record at it, I ran a full white-box pentest against the application. The threat model is not complicated, but "I am the only user" is not a threat model. What I found was bad enough that the app would not have survived a single day of public exposure.

Why bother with a pentest at all

Most personal projects skip security review entirely. The reasoning is usually something like: no one knows it exists, there is nothing valuable in it, and attacking it would take effort. All three of those assumptions are wrong for an app with real personal data.

The app held personal financial records, including payroll figures tied to a real person. Once a DNS record points at something, search engines index it. Misconfigured routes get scanned within hours by automated crawlers that look for predictable URL patterns. "Only I know the URL" stops being true almost immediately after deployment.

Running the pentest before launch, not after, meant that none of the findings spent time on the internet. That distinction matters.

Critical finding 1: real data committed to the repository and served unauthenticated

During development, I had exported a real data file to test PDF generation. That file was committed to the git repository. The repository was not public, but the built application was serving the file at a predictable static URL with no authentication check in front of it. Anyone who knew the URL pattern could download it without logging in.

This was not a theoretical risk. The route existed, the file existed, and the application was already deployed. The data was live on the internet before the pentest began.

Remediation came in two parts. First, the file had to be removed from the repository history, not just from the working tree. A commit that deletes a file does not remove it from the git object store. I rewrote history to excise it and force-pushed to all remotes. Second, the route that served static assets from that directory needed authentication gating before anything else.

The lesson here is simple: never use real data in development, even for a short-lived test. The cost of generating synthetic data is low. The cost of real data appearing in a repository is not recoverable.

Critical finding 2: a personal access token with production write access in plaintext

A GitLab personal access token was embedded in the repository's .git/config file. The token had push access to the main branch. The CI pipeline was configured to build and deploy on every push to main.

Anyone who cloned the repository would have obtained a token that could push code directly to the branch that deploys to production. The attack path is direct: clone, push a malicious commit, the pipeline runs, the production container gets replaced.

The token had been there for a while. It was added during initial setup when I was moving quickly and never cleaned up. This is the most common vector for credential exposure in personal projects: credentials are added for convenience during development and then forgotten.

Remediation: the token was revoked immediately, before anything else. A revoked credential is inert regardless of where copies of it exist. After revocation, I purged it from the git history using the same approach as the data file. All exposed credentials were rotated before I continued the remediation work.

High-severity findings

Three High findings came out of the same assessment.

Edge authentication was documented in the deployment notes as deployed, but it was not actually wired. The application was reachable without passing through the auth layer that was supposed to gate it. This was a documentation drift problem more than a code problem. The config existed; it just was not applied to the correct ingress route.

A debug route in the application was shipping in production. The route was intended for local development and dumped the current session context, including all fields from the active user record. It was not behind any check. Removing it was a one-line change, but finding it required actually looking for it rather than assuming it was development-only.

Session tokens were stored in localStorage with no Content Security Policy header set. A cross-site scripting attack, if one had been present, could have exfiltrated the token directly. localStorage is the wrong place for session tokens in any app that handles sensitive data. Moving to a HttpOnly cookie was straightforward; the absence of a CSP header required adding the header configuration to the web server.

Remediation order

Secrets hygiene came first. Rotate every exposed credential before doing anything else. A revoked token and purged history cost nothing to complete and immediately eliminate the most direct attack paths.

The auth wiring came second, because without it every other fix is cosmetic. An unauthenticated application is an unauthenticated application regardless of how clean the rest of the code is.

The remaining findings were addressed in severity order after those two. The app did not go public until all Critical and High findings were closed and verified.

What this exercise actually teaches

A pentest on a single-user personal project finds things that normal use never exercises. Debug routes, static file handlers, and embedded credentials do not show up in happy-path testing because they exist outside the normal request flow. They only appear when you go looking for them specifically.

The attack surface of a single-user app is not zero. It is smaller than a multi-tenant SaaS, but the data inside it may be just as sensitive. Running the pentest before launch, when findings can be fixed without any exposure, is the only sensible order of operations.