ASVS 5.0 Level 1 checklist
This checklist records the review of Lodger against every Level 1 requirement of
OWASP ASVS 5.0.0. The review ran on 2026-09-24, before v0.2. The
threat model explains the threats behind these controls.
Each requirement has one verdict:
- Pass: the code meets the requirement. The evidence names the code and the test that
proves it.
- N/A: Lodger has no such feature. The evidence says why.
The totals: 56 pass and 14 N/A.
A pull request that adds an endpoint or a new input updates this checklist. Before
v1.0, the new v1.0 features get a new Level 1 review.
V1 Encoding and sanitization
| ID |
Verdict |
Evidence |
| 1.2.1 |
Pass |
Svelte renders text interpolation as text, and web/src has no {@html} or innerHTML. XML comes only from the builders in crates/lodger-core/src/xml/. Header values go through HeaderValue, which refuses control characters. |
| 1.2.2 |
Pass |
Route links encode each VM, pool, and network name with encodeURIComponent, because libvirt allows #, ?, and % in a name. API paths use UUIDs, and the volume, ticket, and console URLs encode their parts. Every link is a relative path. Test: encodes a name that holds URL characters in web/src/routes/vms/page.svelte.spec.ts. |
| 1.2.3 |
Pass |
The server writes JSON only through serde, and the client only through JSON.stringify. Lodger builds no JavaScript at run time. |
| 1.2.4 |
Pass |
Every SQL value in crates/lodger/src/db/ is a bound parameter. The format! calls in SQL insert only 3 time constants. |
| 1.2.5 |
Pass |
Lodger reaches libvirt through the virt bindings, with no subprocess. lodger install runs systemd-sysusers, userdel, and systemctl with fixed argument lists and no shell (crates/lodger/src/install.rs). |
| 1.3.1 |
N/A |
Lodger takes no HTML input. |
| 1.3.2 |
Pass |
web/src has no eval or new Function, and the Content Security Policy has no 'unsafe-eval'. Test: the_csp_allows_the_inline_script_by_its_hash_only. |
| 1.5.1 |
Pass |
Every XML parse goes through xml::parse, which refuses <!DOCTYPE before the parser runs. Test: a_doctype_is_rejected_before_parsing. |
V2 Validation and business logic
| ID |
Verdict |
Evidence |
| 2.1.1 |
Pass |
The section Input rules below. |
| 2.2.1 |
Pass |
crates/lodger-core/src/validate.rs checks names, paths, hosts, bridges, and subnets against allowlists and limits. Formats and modes are enums. The create and change bodies of pools, networks, volumes, and VMs refuse unknown fields. |
| 2.2.2 |
Pass |
Each handler checks its input on the server before it acts. The web app checks only for empty fields. |
| 2.3.1 |
Pass |
Setup runs in a fixed order. Setup must be open, the token must be live and correct, and the name and password must be valid. Then one transaction adds the account. If any account exists, the transaction fails. Tests: parallel_claims_create_exactly_one_account, closed_setup_answers_404_whatever_the_body. |
V3 Web frontend security
| ID |
Verdict |
Evidence |
| 3.2.1 |
Pass |
Every response sends X-Content-Type-Options: nosniff and a CSP with default-src 'self' and frame-ancestors 'none'. The API answers only JSON, and an unknown /api path answers 404, never the HTML page. Test: every_response_carries_the_security_headers. |
| 3.2.2 |
Pass |
The UI shows all outside data, such as VM names, through Svelte text interpolation. |
| 3.3.1 |
Pass |
The cookie is __Host-lodger_sid with Secure, HttpOnly, SameSite=Strict, and Path=/. Test: the_cookie_has_every_protection. |
| 3.4.1 |
Pass |
With built-in TLS, every response sends Strict-Transport-Security: max-age=31536000. Behind a reverse proxy, the proxy must send it. Tests: serve_answers_https_with_the_configured_pair and the plain HTTP health test in crates/lodger/tests/binary.rs. |
| 3.4.2 |
Pass |
Lodger sends no CORS header at all. |
| 3.5.1 |
Pass |
A state-changing request needs Sec-Fetch-Site: same-origin or an Origin equal to public_url, and the session's X-CSRF-Token. Tests: a_change_needs_the_csrf_token_and_the_lodger_origin, an_unsafe_request_from_another_origin_fails_with_403. |
| 3.5.2 |
Pass |
Lodger does not rely on CORS preflight. The Origin rule applies to every method except GET and HEAD, whatever the Content-Type. |
| 3.5.3 |
Pass |
Every change uses POST, PATCH, or DELETE. A WebSocket upgrade needs a ticket from a CSRF-checked POST. |
V4 API and web service
| ID |
Verdict |
Evidence |
| 4.1.1 |
Pass |
Text assets send charset=utf-8, and API answers send application/json, which is always UTF-8. Test: text_types_name_utf_8_and_binary_types_do_not. |
| 4.4.1 |
Pass |
The web app uses wss: whenever the page uses HTTPS. The socket needs the Secure session cookie, which a browser sends over plain HTTP only on loopback. |
V5 File handling
| ID |
Verdict |
Evidence |
| 5.2.1 |
N/A |
Lodger has no file upload. Request bodies stay under axum's default limit of 2 MB. |
| 5.2.2 |
N/A |
Lodger accepts no files. |
| 5.3.1 |
N/A |
Lodger stores no uploads. It serves only the web app that the build embeds. |
| 5.3.2 |
Pass |
Lodger builds no file path from a volume name: libvirt looks the name up inside the pool. Pool paths must be absolute, with no . or .., and outside system folders. Static file paths with .. answer 404. Test: traversal_is_rejected_before_lookup. |
V6 Authentication
| ID |
Verdict |
Evidence |
| 6.1.1 |
Pass |
The section Login throttle below. |
| 6.2.1 |
Pass |
A password needs 15 characters. Tests: short_and_long_passwords_fail, a_common_or_short_password_fails_with_a_clear_message. |
| 6.2.2 |
Pass |
POST /api/account/password and the account page. |
| 6.2.3 |
Pass |
A change needs the current and the new password. Test: a_password_change_needs_the_current_password_and_ends_other_sessions. |
| 6.2.4 |
Pass |
Lodger refuses the 3000 most common passwords that are 15 characters or longer. Test: the_list_has_3000_entries_that_meet_the_length_rule. |
| 6.2.5 |
Pass |
Lodger has no composition rules, only the length and the common list. |
| 6.2.6 |
Pass |
Every password field uses type="password". |
| 6.2.7 |
Pass |
No field blocks paste, and the fields carry autocomplete="current-password" or "new-password". |
| 6.2.8 |
Pass |
Lodger verifies the password exactly as sent. It refuses a password over 1024 characters and never cuts one. |
| 6.3.1 |
Pass |
The login throttle. Tests: the_sixth_failed_login_waits, one_ip_guessing_many_accounts_waits_too, an_ipv6_client_is_counted_by_its_64. |
| 6.3.2 |
Pass |
No default account exists. The first account needs the one-time setup token, or sudo lodger admin create. |
| 6.4.1 |
Pass |
The setup token has 128 random bits, works once, and ends after 60 minutes. The first account sets its own password. Tests: a_token_is_128_random_bits_in_hex, a_token_lives_60_minutes, an_expired_token_fails. |
| 6.4.2 |
Pass |
Lodger has no password hints and no secret questions. |
V7 Session management
| ID |
Verdict |
Evidence |
| 7.2.1 |
Pass |
Every request looks up the SHA-256 of the cookie token in SQLite. Test: protected_endpoints_answer_401_without_a_session. |
| 7.2.2 |
Pass |
Sessions use random reference tokens. Lodger has no static API keys. |
| 7.2.3 |
Pass |
The token has 256 random bits, and the database stores only its SHA-256. Test: a_token_is_256_random_bits_in_hex. |
| 7.2.4 |
Pass |
A login makes a new token and ends the session of the cookie that the browser sent. A password change replaces the caller's session too. Tests: a_login_ends_the_session_that_the_browser_had, a_password_change_needs_the_current_password_and_ends_other_sessions. |
| 7.4.1 |
Pass |
Logout deletes the session. A session ends after 60 idle minutes or 24 hours. Open sockets close within 5 seconds. Tests: login_then_logout_ends_the_session_everywhere, logging_out_closes_the_session_sockets_within_5_seconds. |
| 7.4.2 |
Pass |
Deleting an account deletes its sessions. Test: deleting_the_last_account_fails_and_a_delete_ends_its_sessions. |
V8 Authorization
| ID |
Verdict |
Evidence |
| 8.1.1 |
Pass |
The section Authorization below. |
| 8.2.1 |
Pass |
Every function except health, setup, and login needs a session. Test: protected_endpoints_answer_401_without_a_session. |
| 8.2.2 |
Pass |
Lodger has one tenant, and every account has full rights by design. The only per-user object, the caller's own password, comes from the session and not from the request. |
| 8.3.1 |
Pass |
The session, CSRF, and Origin checks are server middleware. The web app's redirects only change what it shows. |
V9 Self-contained tokens and V10 OAuth
| ID |
Verdict |
Evidence |
| 9.1.1 |
N/A |
Lodger uses no self-contained tokens, such as JWT. |
| 9.1.2 |
N/A |
Lodger uses no self-contained tokens. |
| 9.1.3 |
N/A |
Lodger uses no self-contained tokens. |
| 9.2.1 |
N/A |
Lodger uses no self-contained tokens. |
| 10.4.1 |
N/A |
Lodger is not an OAuth authorization server. |
| 10.4.2 |
N/A |
Lodger is not an OAuth authorization server. |
| 10.4.3 |
N/A |
Lodger is not an OAuth authorization server. |
| 10.4.4 |
N/A |
Lodger is not an OAuth authorization server. |
| 10.4.5 |
N/A |
Lodger is not an OAuth authorization server. |
V11 Cryptography
| ID |
Verdict |
Evidence |
| 11.3.1 |
Pass |
Lodger has no encryption code of its own. TLS uses rustls, which has no ECB or PKCS#1 v1.5 suites. |
| 11.3.2 |
Pass |
The rustls ring provider offers only AES-GCM and ChaCha20-Poly1305. |
| 11.4.1 |
Pass |
SHA-256 hashes the tokens, and argon2id hashes the passwords. SHA-1 appears only inside the WebSocket handshake, which RFC 6455 defines. |
V12 Secure communication
| ID |
Verdict |
Evidence |
| 12.1.1 |
Pass |
rustls allows only TLS 1.2 and TLS 1.3, and it prefers TLS 1.3. |
| 12.2.1 |
Pass |
If TLS is set, Lodger serves only HTTPS and never falls back. Without TLS, Lodger refuses to start on an address that is not loopback, unless trusted_proxies names a reverse proxy with TLS. allow_plain_http = true allows a TLS proxy that is not in trusted_proxies. If no TLS proxy sits in front, that host does not meet 12.2.1. Both cases log a warning at every start, and lodger install refuses the configuration without them. Tests: serve_refuses_plain_http_on_the_network_without_the_opt_in, plain_http_on_the_network_gets_a_warning_that_fits_its_reason, plain_http_on_the_network_stops_the_install_unless_it_sets_tls. |
| 12.2.2 |
N/A |
Lodger is a LAN admin UI, not an external service. If you expose Lodger to the internet, use a publicly trusted certificate. |
V13 Configuration
| ID |
Verdict |
Evidence |
| 13.4.1 |
Pass |
The binary embeds only web/build/, which holds no .git folder. Nothing serves the repository. |
V14 Data protection
| ID |
Verdict |
Evidence |
| 14.2.1 |
Pass |
The session token travels in the cookie, the CSRF token in a header, and passwords in JSON bodies. The WebSocket ticket is the only secret in a URL, because a browser cannot add a header to a WebSocket upgrade. The ticket works once, ends after 30 seconds, and is bound to its session and Origin. |
| 14.3.1 |
Pass |
When the session ends, by logout or by expiry, the web app clears its cached data. The web app uses no browser storage. Test: drops the cached answers when the session ends without a logout. |
V15 Secure coding and architecture
| ID |
Verdict |
Evidence |
| 15.1.1 |
Pass |
SECURITY.md gives the time frames for dependency fixes and updates. |
| 15.2.1 |
Pass |
On 2026-09-24, cargo audit and cargo deny report no advisory. pnpm audit reports 1 low advisory in cookie, a build-time dependency of SvelteKit, which is inside its time frame. |
| 15.3.1 |
Pass |
No answer holds a password hash or a session token. Account answers hold only the ID and the name. No endpoint lists sessions. |
This section covers ASVS 2.1.1. Lodger checks every input on the server with these rules.
- Names of VMs, pools, volumes, networks, and accounts that Lodger creates: 1 to 64
ASCII characters from
A-Z, a-z, 0-9, ., _, and -. The first character
is a letter or a digit. Account names are unique without regard to case.
- Names of objects that other tools created keep their names. API paths use UUIDs, so
such a name needs no check, except that NUL is refused.
- Passwords: 15 to 1024 characters, counted as Unicode characters, and not on the
common list. Lodger has no rule about character types.
- Setup token: 32 hex characters.
- Pool path and NFS export: absolute, at most 4096 bytes, with no
. or ..
segment and no control character. A pool path must not be a system folder. The NFS host has at most 253 characters from A-Z, a-z, 0-9,
., :, and -. Two pools cannot share a path or an NFS export.
- Network subnet: canonical IPv4 CIDR inside
10.0.0.0/8, 172.16.0.0/12, or
192.168.0.0/16, with a prefix of /30 or shorter and no host bits. It must not
overlap another network. A bridge name has 1 to 15 characters and must exist on the
host.
- Volume: format
qcow2 or raw, and a size from 1 MiB to 1 PiB. The name must be
new in the pool.
- VM actions: a fixed list. Force off and delete need the VM name typed again.
- Bodies: JSON only, with a limit of 2 MB, which is axum's default. The create and
change bodies of pools, networks, volumes, and VMs refuse unknown fields.
- Headers: Lodger stores at most 256 characters of
User-Agent. It reads X-Real-IP
and X-Forwarded-For only from an address in trusted_proxies.
Login throttle
This section covers ASVS 6.1.1. The throttle slows password guessing and credential
stuffing, and it never locks an account.
- The throttle covers login and the current-password check of a password change. The
setup token has 128 random bits, so setup needs no throttle.
- Lodger counts failures per account and per client IP. An IPv6 client counts by its
/64. The larger count applies.
- The first 5 failures in 15 minutes cost nothing. After that, each attempt waits
1 second, then 2, 4, and so on, up to 60 seconds.
- Attempts from one IP run one after another. If an attempt must wait more than
60 seconds, it gets
429 Too Many Requests with Retry-After.
- A correct password clears the count of the account. The IP keeps its other failures,
so one good account does not reset guesses at other accounts.
- No account locks, so an attacker cannot lock out the admin. At most 10000 keys per
map stay in memory. A restart of Lodger resets the counts.
- An unknown account costs the same argon2 work as a known one, and it gets the same
message.
- The numbers are fixed in the code. The only related setting is
trusted_proxies,
which decides the client IP behind a reverse proxy.
Authorization
This section covers ASVS 8.1.1. Lodger has one role in v1: every account is an admin
with full rights.
- Without a session:
GET /api/health, setup while no account exists, login, and the
static web app.
- With a session: everything else under
/api. Without a session, the answer is 401.
A state-changing request also needs the session's X-CSRF-Token, or it gets 403.
- WebSockets need a live session, a single-use ticket from that session, and the same
Origin that asked for the ticket.
- Every account can manage all VMs, pools, networks, volumes, and accounts. An account
cannot delete the last account. A password change applies only to the caller's own
account.
lodger admin reset-password and lodger admin create need root on the host.