Security & privacy
ASVS 5.0 Level 1 self-assessment
Completed 25 October 2026
Every Level 1 requirement of the OWASP Application Security Verification Standard 5.0, with what we found and how each one is met. The plain-words summary is on the security page.
What was assessed, and how
- Standard. OWASP ASVS version 5.0.0, Level 1: 70 requirements, taken from the official release file.
- Scope. The EquiDiary web application at app.equidiary.com and its API, which the iPhone and Android apps also load, and the static site at equidiary.com. The platforms underneath — Cloudflare’s, Apple’s, Google’s — are outside it.
- Method. For each requirement we read the code that implements it. Where the result can be seen from outside, we also checked the live service on 25 October 2026: response headers, cookie attributes, TLS handshakes. Many of the controls are exercised by the automated tests that run before every deploy, against a database built from the real schema. Gaps found along the way were fixed and checked again before this result was recorded.
- Who. The person who builds EquiDiary. That makes this a self-assessment, not an independent audit; OWASP does not certify or endorse applications.
- Result. 59 met, 2 met by a different route than the requirement describes (explained below), 9 not applicable.
The requirement names in the tables are our own short summaries; the standard has the exact wording of each.
V1 Encoding and sanitization
| Requirement | Result | How it is met |
| 1.2.1 Output encoding fits its context | Met | Everything shown in the app is HTML-escaped where it is inserted, or set as plain text. File-name headers are built in a restricted ASCII form plus RFC 5987; calendar text is escaped per RFC 5545. The Content-Security-Policy forbids inline scripts as a second line. |
| 1.2.2 URLs built safely | Met | No link is rendered from user input. Phone links are filtered to digits and dial characters, research links are built from the DOI over https, and file links are generated by the server. |
| 1.2.3 JavaScript and JSON encoding | Met | JSON is produced only by the framework’s serializer. No script is built from data, and the pages have no inline scripts. |
| 1.2.4 Database injection | Met | Every query binds its values as parameters. The few SQL fragments assembled in code are constants, never input. |
| 1.2.5 OS command injection | Not applicable | The service runs on Cloudflare Workers, which has no operating-system commands to call. |
| 1.3.1 Rich-text sanitization | Not applicable | No HTML or rich-text input is accepted: everything users type is plain text, escaped on output. Research abstracts from outside are reduced to plain text before they are stored. |
| 1.3.2 No dynamic code execution | Met | No eval, new Function or string timers, and the CSP does not allow them. |
| 1.5.1 XML parsers | Not applicable | No XML is parsed anywhere; every integration speaks JSON. |
V2 Validation and business logic
| Requirement | Result | How it is met |
| 2.1.1 Validation rules documented | Met | A table kept with the code gives the rule for every data item: names (120 characters), notes (4,000), new passwords (8 to 200), real calendar dates, times, prices (0 to 1,000,000,000), ids, lists, and uploads (photos 5 MB, PDFs 10 MB). |
| 2.2.1 Input checked against expectations | Met | Each field is read through a validator that accepts only the expected type, format, range or set — a date that exists, a role from a fixed list, bounded numbers and lengths — and refuses the request otherwise. The database repeats the closed sets as constraints. |
| 2.2.2 Validation on the server | Met | All checks run on the server and in the database. Checks in the app, such as the horse limit or a video’s length, are repeated there. |
| 2.3.1 Steps in order, none skipped | Met | Emailed links work once and are claimed in a single statement. A temporary password allows nothing until it is replaced. A subscription becomes active only when the payment provider says so, never because the browser came back from checkout, and a video counts as ready only when the video service reports it. |
V3 Web frontend security
| Requirement | Result | How it is met |
| 3.2.1 Content not rendered in the wrong context | Met | Uploaded files are served only for the contexts the app uses them in — as an image or video, on their own, or fetched by the app — and refused when requested as a frame, embed, script or stylesheet (judged by Sec-Fetch-Dest). Photos and videos also carry a sandboxed, deny-everything policy, every response carries X-Content-Type-Options: nosniff, and file types are verified at upload. |
| 3.2.2 Text rendered as text | Met | Text reaches the page through textContent or an HTML-escaping function, everywhere. |
| 3.3.1 Secure cookies | Met | The one cookie is __Host-eq_session: Secure, Path=/, no Domain, HttpOnly, SameSite=Lax. Check: sign in and read the Set-Cookie header. |
| 3.4.1 HSTS for at least a year | Met | Every response carries Strict-Transport-Security: max-age=31536000; includeSubDomains. Check: curl -sI https://app.equidiary.com. |
| 3.4.2 CORS restricted | Met | No Access-Control-Allow-Origin header is ever sent, so no other origin can read a response. |
| 3.5.1 Cross-site request forgery | Met | A request that changes anything must come from the app’s own origin, judged from Sec-Fetch-Site and Origin; sibling subdomains are refused too. Only the two payment webhooks are exempt, and they are authenticated by signature. |
| 3.5.2 No way around the CORS preflight | Met | Writes that carry data as JSON must be sent as application/json, which another site cannot send without a preflight — and none is granted. The rest — file uploads and voice dictation, which are multipart forms, and starting a checkout — could be sent by another site without one, so for those the app does not rely on the preflight: they are refused by the origin check in 3.5.1, and a post from another site carries no session cookie in the first place (SameSite=Lax). |
| 3.5.3 Safe methods stay safe | Met | Every change is a POST, PUT or DELETE. GET requests only read; the one exception notes when a calendar feed was last fetched. |
V4 API and web service
| Requirement | Result | How it is met |
| 4.1.1 Content-Type with charset | Met | Every text response names UTF-8, and JSON is served as application/json. |
| 4.4.1 WebSockets over TLS | Not applicable | No WebSockets. |
V5 File handling
| Requirement | Result | How it is met |
| 5.2.1 Upload size limited | Met | 12 MB per request; photos 5 MB, PDFs 10 MB, voice clips 4 MB; a cap on files per entry and per horse; videos at most 3 minutes each, enforced by the video service as well. |
| 5.2.2 Extension and content agree | Met | The type must be on an allowlist (JPEG, PNG, WebP, PDF), the file’s first bytes must match it, and the stored extension comes from the verified type. The uploaded file name is never used. |
| 5.3.1 Uploads never executed | Met | Files are kept in private object storage and only ever streamed back as data. |
| 5.3.2 Safe file paths | Met | Storage keys are generated by the server — area, id, a random part, the extension — and checked against a strict pattern whenever one is read. |
V6 Authentication
| Requirement | Result | How it is met |
| 6.1.1 Anti-automation documented | Met | The operations documentation sets out the bot check, the attempt counters (per address and network, per network, per account), the edge rate limit, and why a stranger cannot lock someone else’s address. |
| 6.2.1 At least 8 characters | Met | 8 minimum and 200 maximum, enforced on the server. |
| 6.2.2 Password can be changed | Met | Account → Change password. |
| 6.2.3 Change needs the current password | Met | The current password is required, and a wrong one counts against the attempt limit. |
| 6.2.4 Common passwords refused | Met | New passwords are checked, case-insensitively, against the 5,000 most common of 8 characters or more, from the UK NCSC’s list. |
| 6.2.5 No composition rules | Met | None. |
| 6.2.6 Password fields masked | Met | Every password field is type=password. The sign-in, registration and administrator set-password fields also have a button to show what was typed. |
| 6.2.7 Paste and password managers allowed | Met | Paste is never blocked, and fields carry the autocomplete hints password managers use. |
| 6.2.8 Password checked exactly as typed | Met | Never trimmed, truncated or case-folded. |
| 6.3.1 Brute-force defences in place | Met | A bot check that fails closed, then an attempt counter claimed in one statement before any hashing: 10 failures per address and network in 15 minutes, 50 per network. Changing the password and deleting the account, which need a session, allow 10 failures per account in the same window. An edge rate limit sits in front. |
| 6.3.2 No default accounts | Met | None exists. The placeholder an empty database starts with has a password hash that cannot match anything. |
| 6.4.1 Initial secrets expire | Met | Emailed links are random (128 or 256 bits), expire (a reset after 60 minutes, a confirmation after 2 days) and work once. A password set by an administrator, or generated for a new account, only lets its holder choose their own, and expires after 3 days. |
| 6.4.2 No hints or secret questions | Met | None. |
V7 Session management
| Requirement | Result | How it is met |
| 7.2.1 Sessions verified by the server | Met | Every API request looks its session up on the server, by a hash of the token, with an expiry check. |
| 7.2.2 Tokens generated per session | Met | A random reference token for each sign-in; no static keys. |
| 7.2.3 Tokens unique, 128 bits or more | Met | 256 bits from a cryptographically secure generator. |
| 7.2.4 New token at every sign-in | Met | Signing in issues a new token and ends the one the browser held. Changing the password issues a new token and ends every other; this device’s previous one answers for one more minute, for requests already in flight. |
| 7.4.1 Ended sessions stay ended | Met | Logout deletes the session on the server, and expiry is enforced in the query that reads it. |
| 7.4.2 Sessions end with the account | Met | Deleting an account deletes its sessions and its calendar link. Roles are read afresh on every request. |
V8 Authorization
| Requirement | Result | How it is met |
| 8.1.1 Access rules documented | Met | Owners, contributors and viewers, per-category sharing, share links and owner-only actions are documented with the code. |
| 8.2.1 Function-level access | Met | Every API route requires a session except three kinds: those that start or recover one (sign-in, registration, the emailed confirmation and reset links, logout, and the check of whether one exists); the two payment webhooks, authenticated by signature; and the share-link routes, authenticated by the link’s token. Every administrative route checks the administrator role. |
| 8.2.2 Data-level access | Met | Ownership and sharing are part of every query’s conditions; someone else’s record answers “not found”. |
| 8.3.1 Enforced on the server | Met | Every rule is enforced by the server. The app only hides what the server would refuse anyway. |
V9 Self-contained tokens
| Requirement | Result | How it is met |
| 9.1.1 Signatures checked before use | Met by a different route | Polar’s webhooks are verified (HMAC-SHA256, constant-time compare). Apple’s signed notifications are never trusted at all: see below. |
| 9.1.2 Algorithms on an allowlist | Met | Algorithms are fixed in code. A token’s own algorithm header is never consulted, so “none” cannot be chosen. |
| 9.1.3 Keys from trusted sources | Met | Keys come from server secrets and fixed hosts. No key or key address carried inside a token is followed. |
| 9.2.1 Validity period honoured | Met | Webhook timestamps must be within 5 minutes. Apple’s signing dates order updates, so an old one cannot overwrite a newer one. |
V10 OAuth and OIDC
| Requirement | Result | How it is met |
| 10.4.1–10.4.5 Authorization server duties | Not applicable | EquiDiary is not an OAuth authorization server: it signs people in with its own email and password only. |
V11 Cryptography
| Requirement | Result | How it is met |
| 11.3.1 No ECB or weak padding | Met | Nothing uses ECB or PKCS#1 v1.5 encryption. The only encryption the service applies itself is to its backups, with age (ChaCha20-Poly1305). |
| 11.3.2 Approved ciphers only | Met | Authenticated encryption only. |
| 11.4.1 Approved hash functions only | Met | SHA-256 for every hash, MAC and signature; no MD5 or SHA-1. |
V12 Secure communication
| Requirement | Result | How it is met |
| 12.1.1 Current TLS versions only | Met | TLS 1.2 and 1.3, with 1.3 preferred. Check: a TLS 1.0 or 1.1 handshake with openssl s_client is refused. |
| 12.2.1 TLS everywhere, no fallback | Met | HTTPS on every host, plain http redirected, HSTS set. The platform’s default workers.dev address is switched off. |
| 12.2.2 Publicly trusted certificates | Met | Certificates from Google Trust Services. |
V13 Configuration
| Requirement | Result | How it is met |
| 13.4.1 No source-control metadata exposed | Met | Only the public folder is deployed: /.git/ and the configuration files answer 404. |
V14 Data protection
| Requirement | Result | How it is met |
| 14.2.1 No secrets in URLs | Met by a different route | The session, passwords and bot-check tokens travel in headers and bodies, and emailed links carry their token in the URL fragment, which browsers never send. Share links and calendar feeds carry a token in the address by necessity: see below. |
| 14.3.1 Device cleared at the end of a session | Met | Logout deletes the offline journal, the queue of unsent changes and the cached photos before it contacts the server; offline, the session itself is ended at the next launch. A launch that finds no session clears them too. |
V15 Secure coding and architecture
| Requirement | Result | How it is met |
| 15.1.1 Remediation time frames documented | Met | A high or critical advisory in code that ships is fixed within 7 days, a moderate one within 30, and dependencies are updated monthly. |
| 15.2.1 Nothing past its deadline | Met | Three packages ship. On 25 October 2026, npm audit reported no known vulnerability in any of them. |
| 15.3.1 Only the fields needed are returned | Met | Responses name the fields they return, account data passes through one allowlist, and prices are masked for anyone not allowed to see them — even when they edit an entry. |
Met by a different route
Links that carry a token (14.2.1)
Three kinds of address hold a secret because the address is the product. A share link is something an owner sends to a buyer or a clinic. A calendar feed is what Google Calendar, Apple’s Calendar or Outlook subscribe to, and they keep only an address. A video’s playback link is signed by the video service, which puts the signature in the path.
What protects them: share and calendar tokens are 128 random bits, stored only as a hash, read-only, and can be revoked or replaced at any moment; a share link can also expire and be limited to some categories or a start date. A video’s link is valid for at most 12 hours and for one video. The service prints nothing to its logs while serving these addresses, query strings are redacted from the logs, and the referrer policy keeps a page’s address out of the requests it makes to other sites.
One weakness remains, and it belongs to Level 2 (4.1.2): a calendar app handed a webcal:// address makes its first request over plain http. It is redirected to https before the token is even looked up, but that first request has crossed the network unencrypted. Calendar apps ignore HSTS, so the only fix on our side would be to stop offering one-click subscription.
Apple’s signed payloads (9.1.1)
Two of Apple’s signed objects reach the server, and the contents of neither are taken from anyone untrusted. A notification about a subscription is read only to learn which subscription to look up; nothing in it is stored. The state is then fetched from Apple’s own API, so a forged notification can at most cause a fresh read of the truth. The transaction inside that API answer arrives over TLS from Apple’s fixed address, in reply to a request we signed, and is held to our app’s bundle id and product list.
Checking Apple’s certificate chain as well would meet the requirement to the letter. That is noted for Level 2.
Beyond Level 1
Level 2 has not been assessed. Some of its requirements are known not to be met yet: there is no second sign-in factor, sessions do not end early when unused, there is no list of signed-in devices, and administrators are not asked for their password again before sensitive actions.
Questions or findings: support@equidiary.com, as described on the security page.
OWASP is a trademark of the OWASP Foundation, Inc. EquiDiary is not affiliated with or endorsed by the OWASP Foundation.