Code Sanity: Project Story
Inspiration
Most security tools assume you already know what you are looking for. They are powerful, but they ship with noisy output, heavy setup and a steep learning curve. We wanted the opposite: a scanner a student or solo developer could open in a browser, point at their own app, and understand in a minute.
Three ideas shaped the project:
- Safe by default. A scanner is only useful if it cannot be turned into a weapon. Local scans stay on loopback, checks are passive, and the "exploit" probes are inert text markers.
- Findings should teach. Every result explains the problem and how to fix it, and says plainly that it is an indicator, not proof.
- Your reports are yours. Results belong to a private account, not a public pastebin.
What it does
Code Sanity crawls up to 10 same-origin pages on a local app, inventories links, query parameters and forms, and checks security headers, database error disclosures and URL reflection. For public sites it makes a single passive HTTPS request, only when the site's CORS policy allows it.
Results appear in a six-tile dashboard: Check website, History, Findings, Site inventory, Export & share and Account & settings. Reports can be saved to your account, compared with earlier runs, marked fixed or accepted risk, and exported as JSON or CSV.
How we built it
The project grew in stages, each one a small pull request.
- A static prototype. Plain HTML, CSS and JavaScript with a scanner that runs entirely in the browser. No build step, no framework.
- A tiny backend. A Python standard-library server to save reports. It validates every report before storing it and never fetches user-supplied URLs.
- Accounts. We first tried a third-party auth provider, then replaced it with built-in accounts so the project has one database and one fewer moving part. Passwords use PBKDF2-SHA256 and session tokens are stored only as hashes.
- Neon Postgres. A thin wrapper lets the same SQL run on SQLite locally and Postgres in production.
- Vercel. A serverless entry point reuses the same request handler, so the local server and the deployment cannot drift apart.
- The dashboard and profile. Hash routing turned one long page into six focused views, and account endpoints let people edit their name, workspace, email and password.
flowchart LR
A[Static prototype] --> B[Python report API]
B --> C[Built-in accounts]
C --> D[Neon Postgres]
D --> E[Vercel deploy]
E --> F[Dashboard and profile]
The math behind the design
Password hashing
PBKDF2 makes each password guess expensive on purpose. With $c = 600{,}000$ iterations of HMAC-SHA256, deriving one key costs roughly $c$ hash-function calls:
$$ \text{DK} = \text{PBKDF2}(\text{HMAC-SHA256},\ \text{password},\ \text{salt},\ c,\ 256) $$
If a single hash takes $t$ seconds, an attacker testing $N$ guesses spends
$$ T_{\text{attack}} \approx N \cdot c \cdot t $$
so raising $c$ scales the attacker's cost linearly. A random 16-byte salt per user means identical passwords produce different hashes, which defeats precomputed tables.
Session tokens
A session token is 32 random bytes, so there are $2^{256}$ possibilities. The chance of guessing a valid one in a single try is
$$ P(\text{guess}) = \frac{1}{2^{256}} \approx 8.6 \times 10^{-78}. $$
Only the SHA-256 digest is stored, so a leaked database does not leak usable tokens.
Rate limiting
Auth and account routes allow $L = 20$ requests per client per hour, using a sliding window. A request at time $t$ is allowed when
$$ \left|{\, s \in R : t - s < 3600 \,}\right| < L, $$
where $R$ is the set of earlier request times. Report mutations use the same rule with $L = 30$.
Risk score
The score is a bounded sum of weighted findings. With severity weights $w_i$ and $n$ open findings,
$$ \text{score} = \min!\Big(100,\ \sum_{i=1}^{n} w_i\Big), $$
so one critical issue matters more than many minor ones, and the result never exceeds 100. The weights are Critical $=40$, High $=25$, Medium $=12$ and Low $=5$. An incomplete scan reports no score rather than a misleading partial one.
What we learned
- Security is mostly restraint. The hardest design decisions were what not to do: no form submission, no redirects, no real exploit payloads, no server-side fetching of user URLs.
- One code path beats two. Making the Vercel function subclass the local handler meant every fix applied to both.
- Standard library goes a long way.
http.server,hashlibandsqlite3covered almost everything. We added one dependency,psycopg, only when Postgres required it. - Error messages are part of the product. A raw
[object Object]on the signup form taught us to treat failure states as real UI. - Encoding matters. A routine whitespace cleanup silently turned
·,—and…into question marks. Always edit UTF-8 files with UTF-8-aware tools. - Browsers enforce real boundaries. CORS means most public sites simply cannot be inspected from a browser, and that is the correct behavior.
Challenges we faced
| Challenge | What happened | How we solved it |
|---|---|---|
| Sign-up worked locally, failed live | The static host had no API, so the response was not JSON and the form showed [object Object] |
Added a Vercel serverless entry point and rewrite, plus clear errors for an unreachable account service |
| Report save returned 404 on Vercel | Vercel's rewrite appends a query string, so comparing the raw request path failed | Compared only the parsed path, and tested with Vercel-style query strings |
| Two databases, one codebase | SQLite uses ? placeholders, Postgres uses %s, and schema upgrades differ |
A small connection wrapper converts placeholders, and migrations branch by driver |
| Read-only filesystem | SQLite cannot be used on Vercel | Made DATABASE_URL required there and documented it |
| Leaked credentials | A live database connection string was pasted into chat | We flagged it immediately, kept it out of every file, and advised rotating it |
| Cluttered single page | Scan, history, findings and settings all competed for space | Restructured into a six-tile dashboard with hash routes and live stats |
| Corrupted characters | A script mangled non-ASCII text in index.html |
Restored them from version control and switched to UTF-8-safe editing |
| Testing without a framework | No test runner was set up | Drove the real server with scripted requests and real browser clicks |
What's next
- Password reset by email.
- Exporting a saved report, not just the current one.
- A shared rate limiter, since the in-memory one is per serverless instance.
- Automated tests and CI.
- Scheduled re-scans with change alerts.
Built with
Python · SQLite · Neon Postgres · psycopg · Vercel · HTML · CSS · JavaScript · Mermaid
Log in or sign up for Devpost to join the conversation.