Security testing report
How ParallelPath has been tested, what was found, and what was fixed. Version 0.1.0-beta, October 2026. Testing was performed by the developer using automated and manual techniques; it is not an independent third-party audit.
Scope and method
| Area | Method |
|---|---|
| Application logic | Automated end-to-end and attack test suite (app/tests/), run on every build |
| Source code | Manual security code review of every route; static analysis with Bandit |
| Dependencies | pip-audit against the PyPI/OSV vulnerability databases |
| Front end | CSP compliance review (no inline script), stored-XSS injection into every text field |
| Deployment | Review of systemd sandbox, AppArmor profile, nginx, coturn, database grants and triggers |
| Privacy | Search of the whole code base and documentation for personal or identifying data; review of what each role and the admin can see |
Testing follows the OWASP Top 10 (2021) categories.
Privacy checks
Automated tests that prove privacy promises, run on every build:
| Check | How it's tested | Result |
|---|---|---|
| Encrypted at rest - database | Known values (email, name, username, family name, child name, message subject/body, event title, file name) are written through the app; then the raw SQLite database file and its write-ahead log are scanned byte-by-byte (UTF-8 and UTF-16) | None found in clear |
| Encrypted at rest - file store | The same scan over every stored blob, including uploaded file contents | None found in clear |
| IP addresses encrypted | Raw database scanned for the client address | Not found |
| Admins can't see family content | Ten unique markers (message, private and shared file names and contents, journal, family/child names, expense, info bank) created by a family; every admin page, the analytics, metrics API and the audit CSV export are loaded and searched | No marker appears anywhere |
| Admins can't see people's identities | The same admin pages searched for the parents' usernames and emails | Only masked forms appear; exact-match search confirms a person without revealing others |
| Admin audit redaction | Admin audit page and CSV checked for family members' usernames | Shown only as "family member"/"person" |
| No account takeover by admins | An admin tries to reset a person's 2FA and obtain a reset link | Refused until the person opts in; person notified and sees the action with the admin's reason |
| Admin session can't reach family routes | Admin cookie used on messages, files, journal, expenses, info, reports, account and export | Redirected to sign-in |
| Private space | Other parent requests private files and journal entries by ID | 404, and not listed |
| Parent's own encrypted export | Export with passphrase decrypted with the documented format (PBKDF2-SHA256 + AES-256-GCM) | Round-trips; short passphrases refused |
| No identifying data in published code or docs | Release packaging scans every file for personal paths, keys and tokens; screenshots use fictional data only | Packaging refuses on any hit |
Plus, by design (verified in the deployment review): encryption at rest is mandatory (the app refuses to start unless its data is on an encrypted volume); web logs are anonymised and never record token-bearing URLs; remote AI providers must use HTTPS; backups contain only ciphertext.
Automated attack suite
40 tests in total, all passing (including the privacy checks above). Highlights:
| OWASP | Test | Result |
|---|---|---|
| A01 Broken access control | Every non-public route requested without signing in | Redirected to sign-in |
| A01 | IDOR: a user in another family tries to read 10 kinds of victim records (messages, events, files, private files, expenses, info bank, journal, reports...) | All refused (404/403) |
| A01 | ...and to change 10 kinds of records (retract a message, reply, edit an event, rename/copy a file, approve an expense, record a payment, edit info/journal, accept a swap) | All refused; data unchanged |
| A01 | The other parent tries to open private files and private journal entries | Refused, and not listed |
| A01 | A read-only professional tries to post, create events/expenses, invite people, start calls, and open areas they weren't granted | All refused |
| A01 | A family user tries every admin-portal page and action | Redirected to admin sign-in |
| A01 | Path traversal in the source viewer, static files and wiki | Refused; no files outside the manifest readable |
| A01 | Open redirect via next/page (//host, /\host, https://, javascript:) |
Only same-site paths accepted |
| A03 Injection | Stored XSS payload in display name, family/child names, message subject/body, event title/location/notes, file name, expense title/notes, info-bank fields, journal, swap note - then 17 pages viewed by both parents | Always escaped |
| A03 | Uploaded HTML with script, requested for preview | Forced download, nosniff, sandbox CSP |
| A03 | SQL injection in every search/filter/sort parameter and sign-in | No effect (parameterised queries) |
| A03 | Spreadsheet formula injection in CSV reports | Neutralised |
| A04 Insecure design | Negative/zero/oversized expense and payment amounts | Rejected |
| A04 | Oversized upload | Rejected (413) |
| A05 Misconfiguration | Security headers and CSP on public and private pages; cross-site POST with foreign Origin, foreign Referer, missing origin, Sec-Fetch-Site: cross-site |
Headers present; all cross-site requests blocked |
| A07 Auth failures | Brute force: sign-in, report verification, calendar-feed token guessing | Rate-limited (429) |
| A07 | User enumeration via sign-in responses | Identical responses |
| A07 | Session fixation (attacker-chosen cookie), cookie flags, re-use of a token after sign-out | Ignored / HttpOnly+SameSite / rejected |
| A07 | Admin and family portals use each other's accounts or cookies | Refused |
| A08 Integrity | Audit row altered directly in the database | Detected by chain verification at the exact row |
| A08 | Report with altered contents checked at /verify | "No match" |
| A09 Logging | Failed sign-ins logged for fail2ban; every action audited | Verified |
| - | Source publication secret-scanner given a file containing an API key | Publication refused |
Findings and fixes
| # | Finding | Severity | Status |
|---|---|---|---|
| 1 | cryptography dependency version allowed by the requirements had 4 published vulnerabilities (PYSEC-2026-3552/3553/3554, GHSA-537c-gmf6-5ccf) |
High | Fixed - minimum version raised to 50.0.0; pip-audit now reports none |
| 2 | nginx regex location for sign-in pages took priority over /admin, so /admin/login would have bypassed the optional admin IP allow-list |
Medium | Fixed - /admin uses a ^~ prefix location with the allow-list on every admin URL |
| 3 | Redirect targets accepted /\host, which browsers treat as another site |
Low | Fixed - strict same-site path check (safe_next) + test |
| 4 | Expense edits and payments accepted zero/negative amounts | Low | Fixed - validated + test |
| 5 | Call-recording chunks had no per-call total size cap (authenticated disk-fill) | Low | Fixed - per-call part count, rate and 4 GB size caps |
| 6 | Audit verification could read cached database rows instead of stored ones | Low | Fixed - verification always reloads from the database + test |
| 7 | Embedded JSON on the call page didn't escape < |
Low | Fixed - <, > and & escaped |
| 8 | Demo data used for documentation screenshots contained real-world place names, a phone area code and coordinates | Privacy | Fixed - replaced with fictional values (and mid-ocean coordinates); screenshots regenerated |
| 9 | An administrator could reset a person's 2FA / obtain a password-reset link and so sign in as them | High (trust) | Fixed - only with the person's opt-in; links emailed directly to the person; person notified + shown every admin action |
| 10 | The admin audit trail showed which person opened which record, their IP and profile changes | Privacy | Fixed - all person/family activity redacted for admins (view and CSV) |
| 11 | Admin account list and inbox showed people's usernames and emails | Privacy | Fixed - masked; exact-match search via encrypted index; inbox anonymous unless the sender opts in |
| 12 | Personal details (emails, names, IPs, devices, notification text) were plain columns inside the encrypted volume | Privacy | Fixed - now individually encrypted columns with a keyed blind index; raw-database scan test added |
| 13 | Full-volume encryption was optional | Medium | Fixed - mandatory in both installers; app refuses to start on an unencrypted volume |
| 14 | Web logs stored full IPs and token-bearing URLs (calendar feeds, invitations, resets) | Privacy | Fixed - anonymised addresses, no query strings/referrers, secret URLs not logged |
| 15 | A LAN AI provider could be configured over plain HTTP | Medium | Fixed - HTTPS required except for localhost |
| 16 | Admin settings page placeholder suggested a plain-HTTP AI URL | Low | Fixed |
Static analysis (Bandit) reports no medium or high issues. Two low informational items were reviewed and are false positives (an empty-string comparison flagged as a "password", and XML-escaping used for PDF output rather than XML parsing).
Accepted risks / by design
- Server operators with root could extract keys from memory - see Security, section 10.
- Account lock-out after 8 failed passwords can be triggered by someone who knows a username (15-minute lock). The per-IP limit and fail2ban reduce this.
- First sign-in sets up 2FA: whoever first signs in with a new account's password enrols the authenticator. Invitations are single-use and short-lived to limit this.
- AI provider URLs are set by admins and fetched by the server; only admins can set them.
- Professionals with message access can read all conversations in the family account, including those with children or family members.
- Development mode (
PP_DEV=1) relaxes cookie and WebSocket origin checks; it must never be used in production (the installer never sets it).
Server-level testing
Performed on a fresh Ubuntu 24.04 LTS server after running the installer (and separately the Docker setup): TLS configuration scan, open-port scan, systemd sandbox exposure score, AppArmor enforcement, container hardening checks, database grant/trigger verification, encrypted-volume refusal test and HTTP header checks. Results are recorded below once completed for this release.
Server-level results: pending for 0.1.0-beta - see the release notes.
Re-running the tests
cd app
python -m pytest tests -n auto --dist loadfile # functional + attack suite
python -m bandit -r parallelpath -ll # static analysis
python -m pip_audit -r requirements.txt # dependency vulnerabilities
Found something? Please report it privately - see Security.