There has been no public release yet. Until v1.0.0, only the current state of the main
branch is supported; an APK from an old Actions run is neither updated nor fixed.
| Version | Supported |
|---|---|
main |
yes |
| APKs from earlier runs and tags | no |
Do not open a public issue. The private channel is Report a vulnerability.
In the report, state:
- what is affected —
server/app/api,server/app/rpcorserver/app/rest(v1 and v2),server/app/bot,server/app/providers/…(petersburg/,netschool/,dadata/or the shareddiary/),android/…or the build; - the commit or the version (
versionNameis visible in the app, under "About"); - the steps to reproduce and the impact you expect;
- whether you want attribution in the advisory.
We aim to acknowledge receipt within 72 hours and to name a plan for the fix once it is reproduced.
Do not send real tokens, class codes or electronic-diary passwords. Replace them with
<redacted>; that does not get in the way of reproducing the bug, and a token that reaches
a conversation has to be revoked either way.
These are invariants, not wishes. Breaking one is a vulnerability even if the app goes on working.
- The owner comes only from the environment.
OWNER_IDSis read from.envand cannot be changed from inside the bot, so nobody can be promoted up to the owner role. - Nobody grants a role equal to their own or above it (
app/bot/roles.py,can_grant). The same function answers an access request through/request, so that an approval cannot route around the rule. - Every step checks again. The state of a multi-step form lives in the database and is driven by the user's client, so a step does not trust the one before it.
- A phone has no rights of its own. A device is bound to a Telegram account by a code,
and any write from the phone is judged by that account's role at the moment of the
request (
services/linking.py). Revoke somebody in the bot and the next press in the app gets a 403.
- Device tokens are random 256 bits, shown once and stored only as SHA-256
(
app/security.py). The database holds no token in the clear. - The Petersburg electronic-diary password is not stored anywhere, in any form: it is
needed for one request, after which the service's own session is what lives on. There is
no column for a password even in the revision that added diary sessions (
0004). - Client addresses are not stored: the rate-limit counter counts by hash
(
client_bucket). BOT_TOKEN,WEBHOOK_SECRET,CRON_SECRETandDATABASE_URLlive in the host's environment or inserver/.env, which is not in the repository. So does the keystore that signs the APK: it comes from theLESSONS_KEYSTORE_*andLESSONS_KEY_*variables or from~/.gradle/gradle.properties.
- Without
WEBHOOK_SECRETthe webhook endpoint is not mounted at all. Telegram signs nothing: it sends JSON, and the bot trusts theuser_idinside that JSON when deciding who may edit a timetable. With no secret, anybody who guesses the URL sends a forged update with somebody else's id — that is not spam, that is taking the bot over. - The comparison goes through
hmac.compare_digest, so the secret cannot be recovered from how long the answer takes. - An empty
CRON_SECRETmeans/api/v1/cron/tickrefuses everyone. A tick anybody can fire is a way to make the bot write to every subscriber on demand.
An attempt to join a class is checked against every class at once, so the rate limit
here is part of the model rather than an optimisation. The counter lives in the database
(on serverless a process remembers nothing), a correct code is not counted against it, and
X-Forwarded-For is honoured for only as many proxies as TRUSTED_PROXY_HOPS declares:
the header is written by the client, and taking it at its word means everyone picks their
own bucket. New codes are eight characters with no O, 0, I or 1.
- The release build talks to its server over
https://only (#202). Itssrc/main/res/xml/network_security_config.xmlrefuses cleartext to every host exceptlocalhostand127.0.0.1, whose requests never leave the phone, and the address sheet refuses anhttp://address where it is typed. The debug build'ssrc/debug/res/xml/network_security_config.xmlkeeps cleartext to every host, for development against a server on the same desk. This used to say plain HTTP was allowed on purpose, for a school's server on a local address, and that what crossed that wire was a class code and a timetable; it was a class bearer, often a write token, the diary bearer and the diary session, which is why the release build stopped allowing it. User certificates are not trusted, so nobody can install their own CA and intercept a configured TLS connection. docs/build.md, «Why a debug build speaks HTTP and a release build does not», has both files and what anhttp://wire carries. - Signing in through GitHub uses the device flow, with no client secret: an APK cannot keep a secret. The client id is set at build time, and without it the sign-in line is not shown.
- The
SCHEDULE_EXACT_ALARMpermission is not requested, and the app degrades to a one-minute window when exact alarms are unavailable.
- A release APK built without a key is signed with the debug key. It installs and it
survives R8, but it must not be published. Not because the key is shared — the alias and
the passwords are constants, but every build machine mints its own key pair and throws it
away, so each CI run signs with a certificate that never existed before — but because
nothing can ever update such a release, not even its own builder, and its certificate,
CN=Android Debug, identifies nobody. This is documented in docs/build.md, «Signing», and the run summary says so — it is a known limitation, not a vulnerability. - The electronic diaries — Petersburg's and «Сетевой город. Образование» — and the
DaData school directory are other people's services, the diaries undocumented. Their own
problems are not ours; our boundary is
server/app/providers/as a whole —petersburg/,netschool/,dadata/and the shareddiary/— together with the app's own diary client, and a vulnerability in that (leaking somebody else's session, reaching for somebody else'sstudent_id, sending a credential to a host it does not belong to) is worth reporting to us. - Somebody with administrator access to the bot can ruin a timetable. That is a role, not a
vulnerability; the vulnerability would be their getting that role around
can_grant.