Security

Last updated · September 2026

How Theioptera is protected, what we have already got wrong and fixed, and what we don't claim. Written for the person deciding whether to trust us with unpublished work.

Reporting a vulnerability

Email yaraismail2611@gmail.com with [security] in the subject line. You will get a reply.

Please don't post an exploitable finding publicly before we have had a chance to fix it. We don't run a paid bounty programme — we are a very small team — but we will credit you by name for anything real, if you want the credit.

Good-faith security research on your own account is welcome and we will not pursue you for it. Please don't test against other people's accounts or data.

How we think about it

Anyone can create an account, so we assume every signed-in user could be an attacker. Signing up earns no trust. The things we protect, in order: other people's projects, chats and uploaded materials; the API keys and database credentials, because those are spendable; and the integrity of the account-to-email mapping, since accounts are keyed by email address.

We would rather write down what is actually true — including what we got wrong — than publish reassurance. The section on past bugs below exists for that reason.

Your account and session

  • Sessions are opaque random tokens generated server-side, not JWTs. They expire after 7 days and are checked on every request.
  • The session cookie is httpOnly, so no script on the page can read it, and Secure in production, so it never travels over plain HTTP. The token itself is never returned in a response body.
  • The cookie is restricted so it is not sent along with requests originating from other sites — the standard defence against a malicious page acting as you.
  • We never see or store a password. Sign-in goes through Google or GitHub, and we never ask you for one.

Because a Google login and a GitHub login for the same address resolve to the same Theioptera account, we accept an email address from either provider only when that provider states it is verified. This is the single most important check in our sign-in code: without it, someone could take over an existing account by signing up at the other provider using your address. We will not add a third sign-in provider without the same guarantee.

Keeping accounts separate

Every query for a project, chat, material or task filters on the account that owns it. Material routes check ownership through the parent project rather than trusting an ID in the URL.

When you ask for something that isn't yours, the answer is an ordinary 'not found', not 'forbidden'. 'Forbidden' would confirm the thing exists, which would let someone map what other people have by guessing IDs.

When you paste a link

You can add a material by URL, which means our server fetches a page of your choosing. That is a capability worth constraining, so it is one of the most carefully written parts of the codebase.

We allow only ordinary web addresses, and we validate the address the connection actually resolves to rather than how it looks — a public-looking hostname pointing somewhere internal is rejected. Internal, private and cloud-metadata addresses are all refused. A site cannot redirect its way past the check: redirects are not followed automatically, and every hop is re-validated. And the address we verified is the address we connect to, so nothing can pass the check and then swap targets.

The site you link to sees a request from our server — not from you. No cookies, no referrer, nothing identifying you.

Content, uploads and what Lio writes

  • Uploaded files are never written to disk, the filename is never used to build a path, and size is capped.
  • Lio's answers are rendered as structured content, never as raw HTML. Links are limited to ordinary web and email addresses. A model — or a paper title it quotes — cannot inject live markup or a script link into your page.
  • Standard protective headers are set on every response, and HTTPS is enforced.
  • Admin-only endpoints refuse to work at all if their secret is unset, rather than falling open. Our public health check reports only 'ok' or 'error', because the underlying error text can contain database credentials.

Spending limits

Calls to language models cost money, so there are two daily ceilings: one per account, and one across the whole platform. Your own limit is checked first, on purpose — charging the shared budget first would let one account looping past its own limit exhaust the platform ceiling and take everyone else down with it.

If we cannot check your personal limit, we let the request through, because a database hiccup should not lock you out of your own work. If we cannot check the platform ceiling, we refuse, because the alternative risks draining the keys. Inconvenience fails open; money and access fail closed.

Bugs we have found and fixed

We keep a dated fix log internally and summarise it here. Publishing the fixed ones costs nothing and is the only honest way to show the process works.

WhenWhatImpact
August 2026A cross-site request check accepted a preview-deployment address pattern that was looser than the one used elsewhere, because the same rule had been written down twice and only one copy was tightened.Not exploitable in production — the cookie policy above independently blocked it — but a defensive layer was silently doing nothing. Both copies were collapsed into one, and the attacker-shaped address is now rejected.
August 2026One secret was compared with an ordinary string comparison while every other secret check used a constant-time one.Low; recovering a secret this way over a network is impractical. Fixed for consistency — there was no reason for one check to be the exception.
August 2026A rate-limit calculation could divide by zero if a budget setting were ever set to zero.Found and fixed before it could occur. It would have broken every chat message rather than exposing anything.

The lesson we took from the first two, and now apply as a rule: never let a security rule exist as two copies. Both were the same root cause — a control defined twice and hardened once.

What we don't claim

We are not ISO 27001 or SOC 2 certified, and we don't have a formal third-party penetration test to show you. Saying otherwise would be easy and false.

We have not signed zero-retention agreements with our AI providers, which means content sent to them sits in an abuse-monitoring window on their side — 30 days at the time of writing — that our own deletion cannot reach. This is stated on the Privacy page too, and it is the honest limit of 'delete my account'.

We keep a list of known-open security items internally and work through it. We don't publish that list, because an unfixed issue described in public is a set of instructions. If you are an institution that needs to see it as part of a review, email us and we will walk you through it directly.

See also
Questions? yaraismail2611@gmail.com