We ran our dashboard app, app.otomoai.com.my, through the OWASP Application Security Verification Standard (ASVS) 5.0.0, Level 1 (all 70 requirements) and wrote down every result. This post is that result, line by line: what passed, what didn't, and what we couldn't check from the code alone.
What ASVS is, and what Level 1 means
The OWASP ASVS project page describes the standard as providing "a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development." It is a public, versioned checklist maintained by the Open Worldwide Application Security Project, not a tool or a paid audit. Version 5.0.0 is the current major version, the first release of the 5.x line.
ASVS groups its requirements into three levels of increasing depth. Level 1 is the level meant to apply to every application, not only those handling higher-risk data, and it is the level this review used in full: all 70 Level 1 requirements, none skipped.
How we assessed it
This was a read-only review of the dashboard app's code, carried out on 14 September 2026 against all 70 Level 1 requirements in ASVS 5.0.0. Every status below is tied to a named file in our code or to a named managed-service control from our database or hosting provider, never a guess. No third party took part; this is our own read of our own code, not an independent audit.
What each status means
- Pass: The requirement is met, and a named file in our code or a named managed-service control is the evidence.
- Not applicable: The requirement describes a mechanism the app doesn't use, so there is nothing in it to check.
- Partial: Part of the requirement is met; the note says exactly what is covered and what is not.
- Not verified: Checking this needs access this review didn't have, usually a setting in our database or hosting provider's dashboard, or a live scan.
- Fail: The requirement is not met today.
The result, chapter by chapter
The file and control references behind each status are held internally, not published here, and are shown to a client on request. The tables below show the requirement, the status, and the note explaining it.
Summary of all 70 ASVS 5.0.0 Level 1 results, 14 September 2026.| Status | Count |
|---|
| Pass | 43 |
| Not applicable | 12 |
| Not verified | 7 |
| Partial | 6 |
| Fail | 2 |
V1: Encoding and Sanitization
V1: Encoding and Sanitization, 8 requirements.| ID | Requirement | Status | Note |
|---|
| V1.2.1 | Verify that output is encoded for its context (HTML, HTML attributes, CSS, HTTP headers, etc.) so the message or document structure cannot be changed. | Pass | Page text is rendered through React, which escapes it automatically, and every API reply is sent as JSON, not raw HTML. |
| V1.2.2 | Verify that untrusted data used to build URLs is encoded for its context, and that only safe URL protocols are permitted (disallow javascript: or data:). | Pass | Links the chatbot can show a customer are limited to web addresses (http/https). Password-reset links are built from the app's own address, not from user-supplied text. |
| V1.2.3 | Verify that dynamically built JavaScript content, including JSON, is encoded or escaped to prevent JavaScript and JSON injection. | Pass | Every API reply is built with JSON.stringify, which escapes the data automatically. |
| V1.2.4 | Verify that database queries use parameterized queries, ORMs, or frameworks protected from SQL and other injection attacks, including in stored procedures. | Pass | Database reads and writes go through our database provider's REST layer using encoded filter parameters, not hand-built SQL text. |
| V1.2.5 | Verify that the application protects against OS command injection, using parameterized OS calls or contextual command-line output encoding. | Not applicable | The backend code runs inside our hosting provider's sandbox, which has no way to run operating-system commands at all. |
| V1.3.1 | Verify that all untrusted HTML input from WYSIWYG editors or similar is sanitized using a well-known and secure HTML sanitization library or framework feature. | Not applicable | The only editable-text component in the app captures plain text, not formatted HTML, so there is no HTML content that needs sanitizing. |
| V1.3.2 | Verify that the application avoids eval() or other dynamic code execution features, and sanitizes any user input included where there is no alternative. | Pass | The code does not build and run JavaScript from text at runtime. |
| V1.5.1 | Verify that XML parsers use a restrictive configuration, with unsafe features like external entity resolution disabled, to prevent XXE attacks. | Not applicable | The app only exchanges JSON, never XML, so there is no XML parser to configure. |
V2: Validation and Business Logic
V2: Validation and Business Logic, 4 requirements.| ID | Requirement | Status | Note |
|---|
| V2.1.1 | Verify that the application's documentation defines input validation rules for checking data items against an expected structure, common or internal. | Partial | Validation rules exist as code, with comments explaining them, but there is no separate written document listing input rules that a non-engineer could read on its own. |
| V2.2.1 | Verify that input is validated against an allow list or expected structure to enforce business rules; L1 can focus on business- or security-critical input. | Pass | The server checks submitted data against defined rules (allowed values, formats, and lengths) before acting on it. |
| V2.2.2 | Verify that input validation is enforced at a trusted service layer; client-side validation helps usability but must not be relied on for security. | Pass | The rules that matter are enforced on the server, so a user cannot bypass them just by editing the browser. |
| V2.3.1 | Verify that the application will only process business logic flows for the same user in the expected sequential step order and without skipping steps. | Partial | Login enforces one clear step order server-side: you must complete two-factor verification before you get in. Other multi-step flows in the app (like appointment booking) were not individually checked for the same kind of step-skipping protection. |
V3: Web Frontend Security
V3: Web Frontend Security, 8 requirements.| ID | Requirement | Status | Note |
|---|
| V3.2.1 | Verify that controls stop browsers rendering a response in the wrong context, via Sec-Fetch-* checks, CSP sandboxing, or Content-Disposition: attachment. | Pass | API responses tell the browser never to render them as a webpage, and downloaded files are marked to save rather than open inline. |
| V3.2.2 | Verify that text-only content is rendered with safe functions such as createTextNode or textContent, to prevent unintended HTML or JavaScript execution. | Pass | Text shown to users goes through React's normal text rendering, not a raw-HTML injection path. |
| V3.3.1 | Verify that cookies have the 'Secure' attribute, and use the '__Host-' or '__Secure-' name prefix. | Not applicable | The app never sets a session cookie, so the cookie-security rule does not apply here; sign-in state is instead sent explicitly as a bearer token on each request. |
| V3.4.1 | Verify that a Strict-Transport-Security header is on all responses, with a maximum age of at least one year; L2 and up must also cover all subdomains. | Pass | Both the app's pages and its API responses tell browsers to always use an encrypted connection, for at least two years, including subdomains. |
| V3.4.2 | Verify that CORS's Access-Control-Allow-Origin is fixed or checked against a trusted allowlist, and a wildcard '*' response never carries sensitive data. | Pass | The customer dashboard's API cannot be read from another website at all. The public chat-widget endpoints only share data with the specific website domains a client has registered. |
| V3.5.1 | Verify that, where CORS preflight is not relied on, sensitive requests are validated as originating from the application itself, to defend against CSRF. | Not applicable | Because sign-in state lives in a header the browser never attaches automatically, a malicious website cannot silently make a signed-in user's browser take an action on their behalf. |
| V3.5.2 | Verify that, where the application relies on CORS preflight for sensitive functionality, no request can reach it without first triggering a preflight check. | Not applicable | Same reasoning as the row above: there is no automatic browser credential for another site to piggyback on, so this CORS-preflight-specific concern does not apply. |
| V3.5.3 | Verify that sensitive functionality uses POST, PUT, PATCH, or DELETE rather than safe methods like GET, or alternatively validates Sec-Fetch-* headers strictly. | Not verified | The handful of routes checked use the right kind of request for actions that change data, but not every route in the app was checked one by one. |
V4: API and Web Service
V4: API and Web Service, 2 requirements.| ID | Requirement | Status | Note |
|---|
| V4.1.1 | Verify that every HTTP response with a body has a Content-Type header matching its actual content, including a safe charset such as UTF-8. | Pass | Every API reply declares itself as JSON, and JSON is always UTF-8 text by specification, so no extra character-set label is needed. |
| V4.4.1 | Verify that WebSocket over TLS (WSS) is used for all WebSocket connections. | Pass | The app's one real-time feature (live calendar updates) only ever connects over an encrypted WebSocket, and the app's own security policy blocks plain, unencrypted WebSocket connections. |
V5: File Handling
V5: File Handling, 4 requirements.| ID | Requirement | Status | Note |
|---|
| V5.2.1 | Verify that the application will only accept files of a size which it can process without causing a loss of performance or a denial of service attack. | Pass | The document-upload endpoint refuses files bigger than 15 MB before doing any real work on them. |
| V5.2.2 | Verify that accepted files have their extension checked against actual content, e.g. magic bytes; L1 can focus on business- or security-critical files. | Pass | Uploaded files are checked byte-by-byte to confirm their real format matches what they claim to be, for every file type where that check is possible. |
| V5.3.1 | Verify that files from untrusted input, stored in a public folder, are not executed as server-side code when accessed directly. | Pass | Uploaded files land in a storage bucket that only serves files back to users; it has no ability to run them as program code. |
| V5.3.2 | Verify that file paths use trusted, internally generated data rather than user-submitted filenames, to protect against path traversal, LFI/RFI, and SSRF. | Pass | The system decides where an uploaded file is saved using its own internal ID, not the filename the user provided, so a crafted filename cannot redirect where the file gets written. |
V6: Authentication
V6: Authentication, 13 requirements.| ID | Requirement | Status | Note |
|---|
| V6.1.1 | Verify that documentation defines how rate limiting and anti-automation defend against credential stuffing and brute force, without malicious account lockout. | Pass | There is a written note describing the rate-limiting approach for this app, and the approach used (a refilling request budget) slows repeated login attempts without ever locking a real user out of their own account. |
| V6.2.1 | Verify that user set passwords are at least 8 characters in length although a minimum of 15 characters is strongly recommended. | Pass | New account passwords must be at least 12 characters, above the 8-character minimum this rule requires. |
| V6.2.2 | Verify that users can change their password. | Pass | Signed-in users have a working "change password" feature. |
| V6.2.3 | Verify that password change functionality requires the user's current and new password. | Pass | Changing a password requires proving you know the old one first. |
| V6.2.4 | Verify that new or changed passwords are checked against at least the top 3000 common passwords matching the application's password policy. | Fail | The live app does not yet check a new password against a breached-password list. A check against the Have I Been Pwned range API was built on 14 September 2026 and ships with the next app deploy; this line changes to Pass then. |
| V6.2.5 | Verify that passwords of any composition can be used, with no rule requiring a minimum count of upper case, lower case, numeric, or special characters. | Fail | The live app still requires an uppercase letter, a digit and a symbol, which ASVS 5.0 forbids. The rules were removed in code on 14 September 2026 (12 to 128 characters only) and ship with the next app deploy; this line changes to Pass then. |
| V6.2.6 | Verify that password fields use type=password to mask entry, though apps may let users temporarily reveal the password. | Pass | Password fields are masked by default; an eye icon lets the user reveal what they've typed if they choose to. |
| V6.2.7 | Verify that "paste" functionality, browser password helpers, and external password managers are permitted. | Pass | Nothing in the code stops users from pasting a password or from using their browser's or a third-party password manager. |
| V6.2.8 | Verify that the application verifies the user's password exactly as received from the user, without any modifications such as truncation or case transformation. | Pass | The password a user types is sent to the authentication service exactly as typed, without being shortened or altered. |
| V6.3.1 | Verify that controls preventing credential stuffing and password brute force are implemented as the application's security documentation describes. | Pass | The rate limits described in the security documentation are actually wired into the login-adjacent endpoints in the code. |
| V6.3.2 | Verify that default user accounts (e.g., "root", "admin", or "sa") are not present in the application or are disabled. | Pass | No built-in "admin" or "root" account with a known password ships with the app. |
| V6.4.1 | Verify that system-generated initial passwords or activation codes are securely random, follow the password policy, and expire shortly after issue or first use. | Not applicable | There is no "temporary starter password" in this system that could linger unexpired: accounts either set their own password at sign-up or receive a one-time reset link instead. |
| V6.4.2 | Verify that password hints or knowledge-based authentication (so-called "secret questions") are not present. | Pass | The app has no "secret question" or password-hint recovery option. |
V7: Session Management
V7: Session Management, 6 requirements.| ID | Requirement | Status | Note |
|---|
| V7.2.1 | Verify that the application performs all session token verification using a trusted, backend service. | Pass | The server checks that a session is genuine by asking the authentication service directly on every request, rather than trusting the browser's claim about who is signed in. |
| V7.2.2 | Verify that the application uses dynamically generated self-contained or reference tokens for sessions, not static API secrets or keys. | Pass | Session tokens come from our sign-in provider's managed service and are unique per login. The app never uses one fixed shared key as a session credential. |
| V7.2.3 | Verify that reference session tokens, if used, are unique, generated by a CSPRNG, and carry at least 128 bits of entropy. | Not verified | Whether the session token has enough built-in randomness is decided inside our sign-in provider's service; that detail is not visible in this codebase. |
| V7.2.4 | Verify that the application generates a new session token on user authentication, including re-authentication, and terminates the current session token. | Pass | Changing your password signs out any other devices that were logged in, and every new sign-in gets a brand-new session token from the authentication service. |
| V7.4.1 | Verify that session termination, such as logout or expiry, disallows further use of that session, by invalidating backend data or tracking terminated tokens. | Pass | Signing out, or being deactivated by an admin, is checked again on the very next action. An old session cannot keep being used. |
| V7.4.2 | Verify that the application terminates all active sessions when a user account is disabled or deleted (such as an employee leaving the company). | Pass | When an admin deactivates a staff account, that account loses dashboard access on its very next request, not just at its next login. |
V8: Authorization
V8: Authorization, 4 requirements.| ID | Requirement | Status | Note |
|---|
| V8.1.1 | Verify that authorization documentation defines rules restricting function-level and data-specific access by permission and resource attribute. | Partial | The rules for who can do what are written down, but as comments inside the code rather than as a separate policy document a non-engineer could review on its own. |
| V8.2.1 | Verify that the application ensures that function-level access is restricted to consumers with explicit permissions. | Pass | Almost every dashboard action checks that the caller is signed in and holds the specific permission for that action; the few routes that skip this check either verify a signature from a trusted webhook sender instead, or are public-facing on purpose. |
| V8.2.2 | Verify that data-specific access is restricted to consumers with explicit permission to that data, to mitigate IDOR and BOLA. | Partial | Any staff member with the right permission can open or edit any record belonging to their own company. There is no further check tying a specific record to a specific staff member. That matches how a shared company CRM is meant to work, since all data in one deployment belongs to one customer, but it means permission rather than per-record ownership is the actual boundary. |
| V8.3.1 | Verify that authorization rules are enforced at a trusted service layer, not in controls an untrusted consumer could manipulate, such as client-side JavaScript. | Pass | Who-can-do-what is decided on the server, so it cannot be bypassed by editing the browser. |
V9: Self-contained Tokens
V9: Self-contained Tokens, 4 requirements.| ID | Requirement | Status | Note |
|---|
| V9.1.1 | Verify that self-contained tokens are validated using their digital signature or MAC to protect against tampering before accepting the token's contents. | Pass | Token contents are only trusted after our sign-in provider's service has confirmed the token's signature is genuine on a live call. |
| V9.1.2 | Verify that only allowlisted algorithms, never 'None', can create or verify self-contained tokens, with extra controls if both algorithm types are supported. | Not verified | This is a setting inside our sign-in provider's own project configuration, which this review could not inspect from the code. |
| V9.1.3 | Verify that key material used to validate tokens comes only from trusted pre-configured sources, with JWT headers like 'jku' checked against an allowlist. | Not verified | Same as above: this lives inside our sign-in provider's managed configuration, not in this codebase. |
| V9.2.1 | Verify that a token is only accepted within its stated validity time span, such as JWT's 'nbf' and 'exp' claims. | Pass | Because the app re-checks the token with our sign-in provider on every single request instead of trusting a locally stored copy, an expired token stops working immediately. |
V10: OAuth and OIDC
V10: OAuth and OIDC, 5 requirements.| ID | Requirement | Status | Note |
|---|
| V10.4.1 | Verify that the authorization server validates redirect URIs based on a client-specific allowlist of pre-registered URIs using exact string comparison. | Not applicable | This rule is for a system that hands out OAuth login tokens to other apps. This app doesn't do that. It only lets users sign in through Google and our sign-in provider. |
| V10.4.2 | Verify that an OAuth authorization code can be used only once for a token request, and reuse causes the server to reject the request and revoke related tokens. | Not applicable | Same as above: this rule applies to an OAuth login-issuing service, which this app is not. |
| V10.4.3 | Verify that the OAuth authorization code is short-lived: up to 10 minutes for L1/L2, up to 1 minute for L3. | Not applicable | Same as above. |
| V10.4.4 | Verify that an OAuth client is only allowed the grants it needs, and that the Implicit and Resource Owner Password Credentials grants are no longer used. | Not applicable | Same as above. |
| V10.4.5 | Verify that the OAuth server mitigates refresh token replay for public clients, preferably with sender-constrained tokens or rotation invalidating used tokens. | Not applicable | Same as above. |
V11: Cryptography
V11: Cryptography, 3 requirements.| ID | Requirement | Status | Note |
|---|
| V11.3.1 | Verify that insecure block modes (e.g., ECB) and weak padding schemes (e.g., PKCS#1 v1.5) are not used. | Pass | The one place the app encrypts data (saved calendar-connection tokens) uses a modern authenticated encryption mode, not an outdated or weak one. |
| V11.3.2 | Verify that only approved ciphers and modes such as AES with GCM are used. | Pass | Same evidence as the row above: encryption uses AES with GCM, which is on the current approved list. |
| V11.4.1 | Verify that only approved hash functions are used for cryptographic purposes such as signatures, HMAC, and KDFs, and disallowed ones like MD5 are never used. | Pass | Every place the code hashes or signs data uses SHA-256, not an outdated algorithm like MD5. |
V12: Secure Communication
V12: Secure Communication, 3 requirements.| ID | Requirement | Status | Note |
|---|
| V12.1.1 | Verify that only the latest recommended TLS versions, such as TLS 1.2 and 1.3, are enabled, with the latest preferred. | Not verified | This is a setting inside our hosting provider's own dashboard for the domain, which this review could not inspect from the code. |
| V12.2.1 | Verify that TLS is used for all client connections to external-facing HTTP services, with no fallback to unencrypted communication. | Pass | The app tells browsers to always use an encrypted connection for at least two years, and the hosting platform serves the app over HTTPS by default. |
| V12.2.2 | Verify that external facing services use publicly trusted TLS certificates. | Pass | The certificate that proves the site's identity to a browser is issued automatically by our hosting provider and is trusted by every standard browser. |
V13: Configuration
V13: Configuration, 1 requirement.| ID | Requirement | Status | Note |
|---|
| V13.4.1 | Verify that the application is deployed without source control metadata such as .git or .svn, or with those folders made inaccessible. | Pass | Our hosting provider only serves the compiled website files from the build output folder, not the project's raw source folder, so the .git history folder is never published. |
V14: Data Protection
V14: Data Protection, 2 requirements.| ID | Requirement | Status | Note |
|---|
| V14.2.1 | Verify that sensitive data is only sent in the HTTP body or headers, never in the URL or query string. | Not verified | The endpoints checked send sensitive data in headers or the message body, not the web address itself, but not every single endpoint in the app was checked one by one. |
| V14.3.1 | Verify that authenticated data is cleared from client storage after the session ends, whether via the Clear-Site-Data header or client-side cleanup. | Partial | Logging out removes the sign-in token from the browser, but a few other pieces of saved data (like bank details cached for the payslip tool) are not explicitly wiped when someone signs out of a shared device. |
V15: Secure Coding and Architecture
V15: Secure Coding and Architecture, 3 requirements.| ID | Requirement | Status | Note |
|---|
| V15.1.1 | Verify that documentation defines risk-based remediation time frames for vulnerable third-party components and for updating libraries in general. | Pass | We publish how fast we fix a confirmed vulnerability by severity, and the same targets apply to vulnerable dependencies flagged by our weekly scan. |
| V15.2.1 | Verify that the application only contains components which have not breached the documented update and remediation time frames. | Not verified | Checking this would need an actual scan of the app's dependencies for known vulnerabilities, compared against a written remediation policy, and neither the scan nor the policy exists yet to compare. |
| V15.3.1 | Verify that the application returns only the required subset of fields from a data object, not the entire object. | Partial | Some parts of the app pick out only the fields they need before sending data back (like the employee profile shown after login). Others, like the contact-edit endpoint, fetch and return the whole database row. A full check across the roughly 85 route handlers was not completed. |
The two Fails
Two requirements fail today, both in password rules (V6.2.4 and V6.2.5). The live app does not yet check new passwords against a breached-password list, and it still requires an uppercase letter, a digit and a symbol, which ASVS 5.0.0 says an application must not require. Both fixes were written in code on 14 September 2026 and ship with the next app deploy; both lines change to Pass once that deploy goes out.
Not verified, and Not applicable
Seven requirements are marked Not verified: not fail, not unknown, just outside what a code review can see. Most of them are settings inside our database and hosting providers' own project dashboards (token signing algorithms, minimum TLS version, key sources), which this review had no access to. The rest need a live check this pass didn't run: an actual dependency-vulnerability scan, and a one-by-one audit of every route file rather than the sample checked here. A full check needs those three things: read access to the database and hosting project dashboards, a live dependency scan, and a route-by-route audit of the roughly 85 API route files.
Twelve requirements are marked Not applicable. Each one describes a mechanism the app simply doesn't have: session cookies, an XML parser, an OS shell, an OAuth authorization-server role. There is nothing in that row for the app to fail or pass; the note on each one says why.
What this is not
- It is a self-assessment, not an ASVS certification. OWASP does not certify applications, and we do not claim to be certified.
- No third party verified any of these results. This was our own read of our own code.
- It covers app.otomoai.com.my only: our dashboard app, not the marketing site or anything else.
- It is a point-in-time result, dated 14 September 2026. We re-run it when the code changes materially, and the two Fails above change to Pass once the fix already written ships in the next app deploy.
Sources