This is a personal portfolio. It has one privileged user, one secret, and a small attack surface — but it does accept file uploads, hold a session, and render content that an authenticated user typed, so it is worth taking seriously.
The latest commit on master is the only supported version. Older tags are historical; fixes go
forward, never back.
Please report privately, through GitHub Security Advisories:
https://github.com/sumitjadhav1703/MacOS-Portfolio/security/advisories/new
Not as a public issue, and not on social media, until there has been a chance to fix it.
There is no bug bounty. This is one person's portfolio and there is no budget behind it. What there is: a genuine thank-you, credit in the advisory if you want it, and a fix.
- What you can do that you should not be able to do
- The smallest steps that reproduce it
- The URL or endpoint, and the method
- Whether it needs an authenticated session
- Anything from a real
.dev.vars, or a realADMIN_PASSWORD_HASH - A session cookie belonging to someone else
- Automated scanner output with no analysis — please say what you think is actually wrong
This is a side project maintained around a degree, so a realistic timeline rather than a flattering one:
| Acknowledgement | within a week |
| Assessment, and whether it will be fixed | within two weeks |
| Fix for something exploitable | as fast as it can be managed |
| Public disclosure | after the fix ships, with credit if you want it |
If you have not heard anything in two weeks, please chase — it means the notification was missed, not that the report was ignored.
- Test against the live site in a way that degrades it for anyone else
- Attempt to brute-force the admin password (there is a lockout; you will just fill a table)
- Access, modify or delete data that is not yours
- Social-engineer anyone
Run it locally instead. npm run worker:dev gives you the whole thing with your own database.
Understanding this makes for better reports.
The public API is read-only and enforced as such. worker/index.ts refuses every method but
GET under /api/. POST /api/ask is the single exception, matched before that guard; it writes
nothing.
Authorization is enforced server-side, in one place. Every route under /admin/api/ passes
through a session check in worker/index.ts before it reaches a handler. The admin SPA's own
route guards are a convenience — they are not what stops anyone. worker/authz.test.ts asserts
this across every route derived from the table specs.
Sessions are opaque 256-bit random ids in D1, not signed tokens. The cookie is HttpOnly,
SameSite=Strict, Path=/admin, and Secure over https. Logout deletes the row, so revocation
is immediate. Mutations additionally require an Origin matching the Worker itself.
The password is PBKDF2-SHA256, 210,000 iterations, compared in constant time, and a malformed
stored hash fails closed rather than open. Deployed Workers refuse a single deriveBits call
above 100,000 iterations, so the work is done in chained rounds that sum to the full count — one
round's output is the next round's key material. Local workerd does not enforce that ceiling,
which is why worker/auth.test.ts asserts the per-call count rather than trusting a green suite. Ten failures from one IP in fifteen minutes locks that
IP out.
Uploads are checked against their own leading bytes, never the Content-Type header and never
the filename. Keys are built from a fresh UUID, so a client-supplied path cannot reach the
storage key. Nothing is deleted from R2 while a content row still references it.
Every write is validated server-side by validate in worker/tables.ts, against the declared
field spec. Anything not declared is dropped rather than written, so id, draft and
created_at cannot be set by a crafted body. Any value that becomes an href goes through one
URL allowlist — http:, https:, mailto: and nothing else.
Unpublished content never enters the process that serves the public. published = 1 is
applied in SQL, and the public bundle is built field by field rather than by spreading a row, so a
draft column cannot leak by accident. The Ask Sumit assistant is a separate Worker with an AI
binding and no database at all — it can only see the bundle it is handed.
The MCP server is read-only by construction. /mcp (docs/mcp.md) requires an OAuth access
token for the /mcp resource with scope mcp:read — the only scope that exists — issued after
the owner signs in with the admin password at /admin/authorize. Its tools are handed a function
that returns the published bundle and nothing else: no database, bucket, KV, secret or admin
handler is in reach, so there is no write path to guard. Inputs are strict and bounded, there is
no outbound fetch, and a browser Origin other than the Worker or the site is refused.
No HTML is ever constructed from content. There is no dangerouslySetInnerHTML anywhere in
the application. React escapes text; the injection surface is URLs, which is why they are the part
that is validated.
| CodeQL | javascript-typescript, python and actions, on every pull request and weekly |
| Dependency review | Blocks a pull request introducing a high or critical advisory |
| Secret scanning | GitHub's, plus npm run scan:secrets locally and in CI |
| Auth and authorization tests | worker/authz.test.ts, worker/session.test.ts |
| Input and upload tests | worker/validate.test.ts, worker/files.test.ts, shared fixtures in worker/security-fixtures.ts |
| Prompt-injection tests | ai/tests/test_security.py, ai/tests/test_api.py |
CodeQL runs the default security-and-quality suite, not security-extended. Extended
roughly doubles the run time and, on a codebase this size, mostly surfaces findings that get
triaged away — which teaches everyone to skim the alerts. If this repository ever grows a real
back end, revisit it.
CodeQL does not analyse legacy/. The single-file prototype in there is the source of truth
for behaviour questions (AGENTS.md rule 9) and is kept for that reason alone: it builds nothing,
serves nothing and is imported by nothing. Its innerHTML writes are real, and they were real in
a page no visitor can reach, so leaving four permanent high alerts in the list would teach exactly
the skimming the paragraph above is trying to avoid. .oxlintrc.json excludes the same directory.
The exclusion lives in .github/codeql/codeql-config.yml; deleting a line there is all it takes
to look again.
Actions are pinned to a major tag, not a commit SHA. Every action used is first-party
(actions/*, github/codeql-action, astral-sh/setup-uv). Blanket SHA-pinning would add
constant churn without adding safety while that stays true. Introducing any third-party action
changes this, and that one should be pinned by SHA.
None of the above proves the application is secure. A green CI run means the checks that exist passed. That is all it means.