Advisory: the private repository was publicly reachable, 2026-09-06 to 2026-09-17
Published 2026-09-18. Incident window 2026-09-06 12:12 to 2026-09-17 about
19:00 (Central European Summer Time). Served at
/docs/advisory-2026-09-17-repository-exposure and linked from
limitations, section 10.
Who this is for. Anyone who used, evaluated or relied on the hosted Operity instance at https://operity.vercel.app. The short version: no users existed during the window, so there is nobody to notify. That is not the same as there being nothing to notify about. The private source, the internal plans and the research data behind every published figure were downloadable by anyone for eleven days, and anyone who fetched them still has them.
Summary
The project's source code is kept in a private repository. For eleven days the
public deployment served the contents of that repository, and some files that
were never in it, to anyone who asked for them by path: /CLAUDE.md,
/docs/PLAN-M8.md, the source under /operity/, the tests under /tests/, and
the whole of the gold-key audit. That audit's material included the full text
of all 160 documents the readers were given, every answer each reader gave for
each field, the adjudicator's verdict and reasoning on every disputed field,
and the known answers themselves. No credential was exposed. No database was
reachable. No user's data existed to expose.
This advisory corrects an earlier account. The first account of this
incident, in the project's internal record, said files whose names begin with
a dot were not served. That was wrong. It was generalised from probing
.env*, .git and .vercel, which the platform withholds by default. Other
dot-folders were served, including .gold-keys/, which held the audit's reader
documents and judgements. Everything below was established by listing the
files of every deployment made during the window, before those deployments
were deleted.
Timeline
| when (CEST) | what |
|---|---|
| 2026-09-06 12:12 | first production deployment. From here on, every deployment served its whole upload by path |
| 2026-09-10 21:30 | the gold-key audit's working files are written to a local folder, .gold-keys/, that is not part of the repository |
| 2026-09-10 22:12 | the next deployment uploads and serves .gold-keys/. So does every deployment after it until the fix, eleven in all |
| 2026-09-17 ~18:45 | found while reading how the documentation was served: /docs/LIMITATIONS.md answered 200 although no route serves that name |
| 2026-09-17 ~19:00 | fixed and deployed: the platform serves only a one-file public/ directory, and files the application does not read are no longer uploaded. An acceptance test sampling 45 repository paths refused the old deployment with 39 of them and passes on the new one |
| 2026-09-18 | this advisory; the gold-key pool retired (below) |
| 2026-09-18 | every deployment made before the fix, 22 of them, deleted, so none can be restored; the platform's access token for automated requests revoked (below) |
What was reachable
Everything uploaded with a deployment, by its path in the project folder:
- Every file in the repository: the application source, the tests, the
migrations, the deployment runbook, the internal milestone plans and review
records (
CLAUDE.md,docs/PLAN-M8.md),docs/KEYS.md(public keys and the key-handling procedure; no private key is in it), the reports and the data files behind them, and the gold-key worksheet (docs/gold-key-worksheet.csv). - The whole gold-key audit, from 2026-09-10 22:12. The audit measured how
often a gold item's known answer is not determined by its document. Nothing
of it was withheld:
- In the repository:
docs/gold-key-worksheet.csv, all 160 documents with the known answer for each field;docs/REPORT-GOLD-KEYS-readings.json, every answer each of the two blind readers gave, field by field, with whether they judged the document unambiguous; anddocs/REPORT-GOLD-KEYS-adjudications.json, the adjudicator's verdict and reasoning on each of the 24 fields the readers disputed. - In
.gold-keys/, a local folder that was never in the repository:reader_documents.json, the full text of the 160 documents exactly as the readers received them;batches/batch_01.txttobatch_16.txt, the same documents in the sixteen batches the readers worked through; andadj_conventional.json, the adjudicator's verdicts and reasoning on the same 24 fields under the conventional reading standard.
- In the repository:
- Other local files that were never in the repository: the test runner's
and linter's caches (
.pytest_cache/,.ruff_cache/), in every deployment..env.example, which holds no values, was uploaded and answered 404. The file lists are complete: every deployment made during the window was listed file by file on 2026-09-18 before it was deleted, so no earlier upload is unaccounted for.
What was not exposed, and how that was established
- No secret. The production credentials (database passwords, the passport signing key, the email provider's key, the scheduler's secret) live in the hosting platform's encrypted settings and in the operator's keychain, never in the project folder. The repository's whole history is scanned for key-shaped strings on every build (13 patterns, each with a test proving it fires), and the same scanner found nothing in the 50 served files that are not in the repository.
- No environment file, and no git data.
.env.local,.env.example,.git/,.vercel/,.gitignoreand compiled Python files were requested from the last deployment before the fix, through the platform's authenticated access, and every one answered 404: the platform excludes those names by default. That default covers only those names. It did not cover.gold-keys/, and it is why the first account of this incident was wrong. - No database, and no user data. Files were served, not queries. The database was never reachable through this path, and no account other than the operator's and the acceptance suite's existed before sign-up opened on 2026-09-16 and 2026-09-17.
- No live gold answer. Production held no gold items at all during the window (counted directly). The exposed gold material was research data, and it is now retired (below). The published detection figures were not gamed by it: the dishonest sellers they measure are programs written in advance, and none of them reads anything outside the job it is given.
- The exposure has ended, and cannot be restored. The public address serves the fixed deployment. All 22 deployments made before the fix were deleted on 2026-09-18, so none remains to roll back to, and the address of every one of them now answers 404. The platform's automation token, which let a scripted request past the login on a deployment's own address and was how the old deployments were examined, was revoked the same day. The three deployments kept were each listed file by file and serve none of the repository.
What we cannot tell you: who downloaded what. The platform keeps request logs for hours, not days, so there is no record of the requests made during the window. Treat everything listed above as public.
Root cause
A platform default nobody decided. The deployment is configured with no web framework and no output directory. For a project configured that way, the hosting platform treats the whole uploaded folder as the website's static files, and it serves a matching file before it consults the rule that sends every request to the application. Nobody chose that; it is what happens when the setting is absent. The upload was the whole project folder, so the website was the whole project folder. The same shape has produced this project's other serious defects: a signing key nothing read, so the published default signed every passport; and a scheduled routine nothing scheduled.
What changed
- The platform's static output is now a directory holding one file
(
robots.txt). Every other path reaches the application, which answers 404 for anything it does not serve. - Only what the application reads is uploaded: the exclusion list is now an allow-list, so the tests, the acceptance suite, the scripts, the internal documents and every local folder are left off the platform unless named. A test proves every file the application reads is still uploaded, and that each excluded file really is excluded. It is matched by the exclusion list alone, not by the repository's own ignore file, which had been hiding two gaps in that test.
- The acceptance suite run against each deployment requests 45 repository paths and fails unless each answers the application's own 404.
- Every deployment made before the fix is deleted, so a rollback cannot republish the repository, and the automation token that opened those copies is revoked.
- The gold-key pool is retired. Every document in the exposed worksheet and in the audit's working files (the same 160) is recorded by its fingerprint. The verifier refuses to create or activate a gold item from any of them, and reconciliation of the production database fails if one exists. A published answer cannot measure anyone. Production held no gold items when this was checked (2026-09-18 07:08 UTC). Gold items on this instance are made from real buyers' documents, and the research harness that generated the retired pool now draws its gold from a new seed.
What you need to do
Nothing. There were no users to act, and no credential, key or account to rotate because of this. If you are evaluating Operity, assume its source code and internal plans are public. Its security was designed on that assumption anyway: every threshold is published, and no check depends on a secret parameter.