Download Latest Version OpenWhistle 2.1.0 source code.zip (4.7 MB) Google Add to Preferred Sources
Home / v2.1.0
Name Modified Size InfoDownloads / Week
Parent folder
OpenWhistle 2.1.0 source code.tar.gz 2026-09-27 4.5 MB
OpenWhistle 2.1.0 source code.zip 2026-09-27 4.7 MB
README.md 2026-09-27 15.8 kB
Totals: 3 Items   9.2 MB 0

Upgrade notes

  • Migration 009 adds must_change_password to every account, false for existing ones: nothing records who set their password.
  • Migration 010 recomputes every feedback deadline by §17 Abs. 2 HinSchG: three calendar months from the acknowledgement or, without one, three months and seven days from receipt. Unacknowledged reports get a deadline for the first time, and their reminders start.
  • Migration 011 gives every account an organisation, the default one where it had none.
  • Migration 012 records who made each account (created_by_id), from the audit log.
  • An unedited docker-compose.yml or .env.example install no longer starts. Compose now reads .env, and no example key passes the 32-character check. Set SECRET_KEY and ENCRYPTION_KEY.
  • Images are published only after CI, E2E and the security scans pass on the tagged commit, and only for a tag on main. A release tag without DOCKERHUB_TOKEN now fails.
  • latest, X and X.Y move only to the highest stable release in their line: never to a pre-release, never back to an old tag being re-published.

Added

  • Every admin changes their own password, and must when someone else set it. My account in the sidebar opens /admin/account for every role: username, role, organisation, login methods. A password change (POST /admin/account/password) needs the current password, the new one twice and a current TOTP code, counts wrong guesses towards the sign-in lockout, ends every other session of the account and is audited (auth.password_changed). An account whose password an admin chose on /admin/users, a superadmin reset or the host's reset script set must change it before any other admin page opens; a new or reset account enrols its authenticator first. LDAP and SSO accounts see where to change theirs. In DEMO_MODE the demo accounts keep theirs.
  • Admins link their own single sign-on identity. Signed in with password and TOTP, choose Link single sign-on on /admin/account (POST /admin/oidc/link); Unlink single sign-on removes it. The link request is bound to that session and to the purpose "link" in Redis, so a login state never links and a link state never signs in. An identity linked to another account is refused. Both are audited (auth.sso_linked, auth.sso_unlinked).
  • A lost authenticator can be reset. A superadmin clicks Reset authenticator on /admin/users (POST /admin/users/{id}/reset-totp): the account becomes new again. The old app and the old password stop working, the user's sessions end, and the superadmin sees a random temporary password once, to hand over; with it, the next login enrols a new app at /admin/mfa/setup. The password is in no log and no audit entry. LDAP and SSO accounts keep their first factor. Not for one's own account, and not for the demo accounts in DEMO_MODE. On the host, scripts/reset_admin_password.py --reset-totp <username> prints a new secret and otpauth:// URI once, for any account including the last superadmin. Both are audited (admin.totp_reset).

Documentation

  • The blog is in English and German. Every article has its twin in the other language, linked both ways by hreflang and a language switch; the English index is /blog/en.html, and the English pages link their Blog item there. The German URLs are unchanged. test_every_blog_page_exists_in_english_and_german holds it for new articles.
  • New article: removing metadata without altering the evidence photo, in English and German: what the bug bounty measured, and why no test noticed.

  • A changelog page on the website, rendered from this file (docs/changelog.html, checked by tests/test_changelog_page.py).

  • Diagrams and screenshots in the documentation, in the reader's theme: architecture, submission flow, case lifecycle and login as Mermaid sources rendered to SVG, and the main pages as screenshots re-taken by scripts/take_screenshots.py.
  • The user documentation is rewritten to measured prose limits (no sentence over 30 words, average under 18) and corrected where it contradicted the code: OIDC logins also need TOTP, a correct PIN always opens its report, the app sets the security headers, case numbers are random.
  • CONTRIBUTING.md carries the documentation rules; guard tests hold them.
  • The installation example now puts the Redis password into REDIS_URL. The production Redis runs with --requirepass "${REDIS_PASSWORD}", so the documented redis://redis:6379/0 could not connect. .env.example now pins OPENWHISTLE_VERSION 2.0.1; docs.html no longer names the default, which had gone stale at 2.0.0. The retention section says what HinSchG §11 Abs. 5 says: deletion three years after the procedure ends, not a three-year minimum.
  • Both landing pages are rewritten for "open source whistleblower software" and "kostenloses Hinweisgebersystem". Claims the code or the statute contradict are gone: "100 % HinSchG-konform", rate limits "never IP-based", "OIDC or TOTP", and competitor prices without a source.
  • A comparison page, open-source-whistleblowing-software.html: OpenWhistle, GlobaLeaks, SecureDrop and Hush Line on licence, hosting, deadlines, Tor, languages and audits, each sourced.
  • A new German article, blog/interne-meldestelle-kostenlos.html: what a free system still costs. The German comparison article keeps only vendor-published facts, each sourced and dated; the three older articles carried 2025 as publication date, they were published 2026-04-27.
  • The German pages are re-targeted to the queries Search Console shows. "Hinweisgebersystem Software", "Hinweisgebersystem Anbieter (Vergleich)" and "HinSchG Software" had about 1,300 impressions and no click in three months, at positions 15 to 29. de/index.html now leads with "Hinweisgebersystem Software", the comparison article with "Hinweisgebersystem Anbieter Vergleich" and seven selection criteria. A new article, blog/hinweisgebersystem-dsgvo-eu-hosting.html, answers the EU-hosting, encryption and GDPR questions (positions 9 to 10) in its first sentence. Both landing FAQs add the question-style queries.
  • Every page has a search head: title of at most 60 characters, unique description, canonical, hreflang, Open Graph, JSON-LD. docs/sitemap.xml is rendered from the heads by scripts/render_sitemap.py; missing paths get 404.html. tests/test_seo.py holds all of it.
  • The share image is real. og-image.png was a blank navy rectangle, so every shared link showed an empty card. The touch icon is now 180×180.
  • HinSchG citations are corrected and checked by tests/test_hinschg_citations.py: the pages cited a third Absatz of § 17 and a seventh of § 16, which do not exist; deletion is § 11 Abs. 5, not § 26; a missing reporting office costs up to 20,000 € (§ 40 Abs. 2 Nr. 2, Abs. 6), not 50,000 €.

Security

  • Three code-scanning alerts closed before this release. Draft and status-session ids were checked with ^…$ and re.match, and $ also matches before a trailing newline; every such check is fullmatch now (#107). The language switch and draft-cookie alerts were shown not exploitable by tests and dismissed with that evidence.
  • Every request path and query string reached the container's stdout, the setup token and OIDC codes included. --no-access-log empties uvicorn's access handlers; importing the app gave them back. Measured with the Dockerfile's own command: "GET /setup?token=… HTTP/1.1" 200.
  • One TOTP code opened two sessions. pyotp compares after Unicode normalisation, so 575203 (fullwidth) matched a used 575203 while the single-use key held the raw string. Codes are six ASCII digits; one code authenticates one action, sign-in after enrolment included.
  • The setup wizard stored any TOTP secret the hidden field sent: AAAA became the first superadmin's second factor, !!!! was a 500.
  • A plain admin could re-enable a superadmin another superadmin had disabled; in DEMO_MODE any visitor could deactivate or demote a demo account and lock out everyone after them.
  • Four eyes were two accounts. An admin could make a second admin, sign in with the password they had just chosen, and confirm their own deletion request. An account and the accounts it made, directly or through others, no longer confirm each other.
  • A confirmed deletion left no trace. report.delete_confirmed had labels in every locale and was never written, and the report's own entries go with it. It is written now, with case number, requester and confirmer.
  • Office files kept the Windows account name in the saved-folder path (absPath), template and link paths (C:\Users\<name>\…), fileSharing, SharePoint columns, revision-session ids and document variables. PDF XMP on pages, images and fonts (dc:creator) and editor PieceInfo survived; a PDF with embedded files is now refused. A JPEG's motion-photo video or JFIF thumbnail rode along after the image.
  • A session refreshed during a password change survived it: the refresh stored its new token after the sweep had passed.
  • The TLS private key was 0644 until its chmod; it is 0600 from the first byte.
  • Quay lost releases 1.3.1, 1.4.0 and 1.5.0. The weekly cleanup deleted old sha- tags by digest, which deletes every tag on that digest; 2.0.0 was three pushes from the same fate. It now deletes only digests no kept tag uses.

Fixed

  • Switching LDAP on locked out every local account, the setup wizard's superadmin included. A directory user named like a local account was a 500; an entry without LDAP_ATTR_USERNAME provisioned ALICE next to alice. The lockout counted alice, Alice and ALICE separately and expired from the first wrong password: ten spread over 29 minutes locked for one.
  • The §17 HinSchG deadlines were computed five ways. +90 days is a day late for an acknowledgement on 31 January or 1 February; a case moved to "in review" without acknowledging had no feedback deadline and no reminder; 12 hours before the deadline the dashboard said overdue and the case page 0 days left; the PDF called 7 days 12 hours compliant; the 7-day rate counted a report received today as missed. One module computes them now.
  • Photos and screenshots are stored as taken. A palette PNG came out black, an animated PNG or WebP kept one frame, a 5.5 MB JPEG grew to 14.4 MB, past the limit. JPEG, PNG and WebP are cleaned by dropping metadata segments, pixels untouched; iPhone MPO photos are accepted.
  • One upload could stall the server: a 450 KB PNG cost 1.1 GB of RAM, a 518 KB GIF 26 s of CPU. PNG, JPEG and WebP are no longer decoded, GIF and TIFF are capped at 50 megapixels, and cleaning runs off the event loop. A file over 10 MB after cleaning is refused.
  • Multi-tenancy could not give an organisation its own admin: a new account took its creator's organisation. Accounts made before multi-tenancy was switched on, or by LDAP, had none and then saw no case. A superadmin chooses the organisation on /admin/users.
  • Text at its maxlength was refused or cut for its line breaks. Browsers send CRLF; maxlength counts one (Chromium: 20 characters sent as 24). An admin reply took any length; a long category slug, location code or organisation name, or a large sort order, was a 500.
  • A mistyped notification address was stored: the form is novalidate, so type=email checked nothing, and the mail never came. A reply to a closed case was stored from a page left open; it is refused and the whistleblower told. A lower-case case number was refused.
  • The audit export ignored the page's filters and stopped at 10 000 rows. Taking it, downloading an attachment and unassigning are now recorded; a superadmin's search is recorded in every organisation whose reports it read.
  • TZ moved the "03:00 UTC" jobs: with TZ=Europe/Berlin retention ran at 01:00 UTC. The retention page named tomorrow for tonight's run between 00:00 and 03:00.
  • The digest said "1 new message" for three replies on one case; it counts cases and now says so.
  • A non-ASCII CSRF token was a 500, and two ow_csrf cookies refused every form.
  • The username fields refused ., @ and spaces the server accepts.
  • Retention left the status sessions of the cases it deleted in Redis.
  • Unlinking SSO was allowed when LDAP had been switched off and the link was the only way in.
  • The Quick Start ignored .env: docker-compose.yml hard-coded SECRET_KEY and set no ENCRYPTION_KEY.
  • The publish job could call a tag it could not read "verified": a failure inside for x in $(…) does not trip set -e.
  • The Ansible role could not finish a TLS install or renew its certificate (nginx held port 80 before the certificate existed), and its .env changed values containing $.
  • helm upgrade with changed values restarted no pods, reset the HPA's replicas, and the ingress refused uploads over 1 MB.
  • /static/ was not rate-limited: 80 parallel requests, 80 × 200; now 49 get 429, as on /.
  • Helm, Ansible and .env.example shipped the old brand colour #0f4c81.
  • CI's nginx pin could never be updated: Renovate matched none of the three docker run pins.
  • A test patched asyncio.create_task and left a MagicMock in the notification queue, failing later tests depending on order.

  • Admin lists hid their actions behind a sideways scrollbar. Users, categories and locations sat in two thirds of the page, as organisations did in 2.0.1: the role select read "Falll" and the categories' actions were out of view at 1920 px. Every list now runs under its form, across the full width, and table cells are 0.75 rem a side, so the German dashboard's eight columns fit 1,064 px.

  • OIDC login could never succeed. Nothing ever wrote an account's OIDC sub and issuer, so every SSO login ended in "no account is linked". Linking now exists (see Added).
  • A lost authenticator locked its admin out for good. The reset script kept the TOTP secret, and the docs said to edit the database. See Added.
  • No session is accepted for an account whose authenticator awaits enrolment. A reset takes effect in the same commit, before its sessions are swept from Redis.
  • Whoever set a password for someone else knew it for good. No page let an admin change their own password, so the admin who created an account, or the superadmin who reset it, kept knowing it. See Added.
  • The host's password reset left the account's sessions running and the audit log empty. reset_admin_password.py --username now ends every session of the account and records admin.password_reset, never the password.
  • Four form fields skipped their format check in Chrome. Browsers compile pattern with the v flag, where a bare - closing a character class is a syntax error; the new-user, location, organisation and setup forms logged it and checked nothing. Now escaped, held by tests/test_pattern_attributes.py.
  • The sidebar's Log out sat 12 px left of the links above it, and the second panel of a two-column admin page (users, categories, locations) started 20 px below the first.

Removed

  • /admin/demo/reset. It reset nothing (the seed only adds what is missing) and no page linked to it.
  • app/schemas. No route used it; it looked like the validation the wizard was missing.
Source: README.md, updated 2026-09-27