EquiDiary
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

The requirement names in the tables are our own short summaries; the standard has the exact wording of each.

V1 Encoding and sanitization

RequirementResultHow it is met
1.2.1 Output encoding fits its contextMetEverything 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 safelyMetNo 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 encodingMetJSON 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 injectionMetEvery query binds its values as parameters. The few SQL fragments assembled in code are constants, never input.
1.2.5 OS command injectionNot applicableThe service runs on Cloudflare Workers, which has no operating-system commands to call.
1.3.1 Rich-text sanitizationNot applicableNo 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 executionMetNo eval, new Function or string timers, and the CSP does not allow them.
1.5.1 XML parsersNot applicableNo XML is parsed anywhere; every integration speaks JSON.

V2 Validation and business logic

RequirementResultHow it is met
2.1.1 Validation rules documentedMetA 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 expectationsMetEach 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 serverMetAll 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 skippedMetEmailed 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

RequirementResultHow it is met
3.2.1 Content not rendered in the wrong contextMetUploaded 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 textMetText reaches the page through textContent or an HTML-escaping function, everywhere.
3.3.1 Secure cookiesMetThe 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 yearMetEvery response carries Strict-Transport-Security: max-age=31536000; includeSubDomains. Check: curl -sI https://app.equidiary.com.
3.4.2 CORS restrictedMetNo Access-Control-Allow-Origin header is ever sent, so no other origin can read a response.
3.5.1 Cross-site request forgeryMetA 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 preflightMetWrites 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 safeMetEvery 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

RequirementResultHow it is met
4.1.1 Content-Type with charsetMetEvery text response names UTF-8, and JSON is served as application/json.
4.4.1 WebSockets over TLSNot applicableNo WebSockets.

V5 File handling

RequirementResultHow it is met
5.2.1 Upload size limitedMet12 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 agreeMetThe 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 executedMetFiles are kept in private object storage and only ever streamed back as data.
5.3.2 Safe file pathsMetStorage 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

RequirementResultHow it is met
6.1.1 Anti-automation documentedMetThe 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 charactersMet8 minimum and 200 maximum, enforced on the server.
6.2.2 Password can be changedMetAccount → Change password.
6.2.3 Change needs the current passwordMetThe current password is required, and a wrong one counts against the attempt limit.
6.2.4 Common passwords refusedMetNew 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 rulesMetNone.
6.2.6 Password fields maskedMetEvery 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 allowedMetPaste is never blocked, and fields carry the autocomplete hints password managers use.
6.2.8 Password checked exactly as typedMetNever trimmed, truncated or case-folded.
6.3.1 Brute-force defences in placeMetA 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 accountsMetNone exists. The placeholder an empty database starts with has a password hash that cannot match anything.
6.4.1 Initial secrets expireMetEmailed 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 questionsMetNone.

V7 Session management

RequirementResultHow it is met
7.2.1 Sessions verified by the serverMetEvery 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 sessionMetA random reference token for each sign-in; no static keys.
7.2.3 Tokens unique, 128 bits or moreMet256 bits from a cryptographically secure generator.
7.2.4 New token at every sign-inMetSigning 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 endedMetLogout deletes the session on the server, and expiry is enforced in the query that reads it.
7.4.2 Sessions end with the accountMetDeleting an account deletes its sessions and its calendar link. Roles are read afresh on every request.

V8 Authorization

RequirementResultHow it is met
8.1.1 Access rules documentedMetOwners, contributors and viewers, per-category sharing, share links and owner-only actions are documented with the code.
8.2.1 Function-level accessMetEvery 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 accessMetOwnership and sharing are part of every query’s conditions; someone else’s record answers “not found”.
8.3.1 Enforced on the serverMetEvery rule is enforced by the server. The app only hides what the server would refuse anyway.

V9 Self-contained tokens

RequirementResultHow it is met
9.1.1 Signatures checked before useMet by a different routePolar’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 allowlistMetAlgorithms are fixed in code. A token’s own algorithm header is never consulted, so “none” cannot be chosen.
9.1.3 Keys from trusted sourcesMetKeys come from server secrets and fixed hosts. No key or key address carried inside a token is followed.
9.2.1 Validity period honouredMetWebhook 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

RequirementResultHow it is met
10.4.1–10.4.5 Authorization server dutiesNot applicableEquiDiary is not an OAuth authorization server: it signs people in with its own email and password only.

V11 Cryptography

RequirementResultHow it is met
11.3.1 No ECB or weak paddingMetNothing 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 onlyMetAuthenticated encryption only.
11.4.1 Approved hash functions onlyMetSHA-256 for every hash, MAC and signature; no MD5 or SHA-1.

V12 Secure communication

RequirementResultHow it is met
12.1.1 Current TLS versions onlyMetTLS 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 fallbackMetHTTPS on every host, plain http redirected, HSTS set. The platform’s default workers.dev address is switched off.
12.2.2 Publicly trusted certificatesMetCertificates from Google Trust Services.

V13 Configuration

RequirementResultHow it is met
13.4.1 No source-control metadata exposedMetOnly the public folder is deployed: /.git/ and the configuration files answer 404.

V14 Data protection

RequirementResultHow it is met
14.2.1 No secrets in URLsMet by a different routeThe 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 sessionMetLogout 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

RequirementResultHow it is met
15.1.1 Remediation time frames documentedMetA 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 deadlineMetThree packages ship. On 25 October 2026, npm audit reported no known vulnerability in any of them.
15.3.1 Only the fields needed are returnedMetResponses 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

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.