SurfWave
Security
The limits of the server, their settings, and what an app must do itself.This page as MarkdownStatuscurrent on 2026-11-03.
Related documentsBuild a full app, SurfWave reference, limits of version 0, ADR-0109
This page lists the limits of the server of surf build --target server, their settings, and what an app must do itself.The server reads these environment variables when it starts. A value that is not valid stops the start.The server also refuses these requests, and it closes the connection after the answer:Each response has X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. A hook after can set another value. The server sends no Content-Security-Policy, because a policy depends on the app. A page with a later value has an inline script.The binary has no TLS. A public site needs a proxy in front of it. The proxy must do these tasks:
Variable
Default
Meaning
HOST
127.0.0.1
The address of the server. With the default, only this machine reaches the server, for example a proxy on the same machine. A container needs 0.0.0.0, and the Dockerfile of --adapter docker and --adapter fly sets it (ADR-0109).
PORT
3000
The port.
ORIGIN
none
The address of the site in the browser, for example https://example.com. A site behind a proxy needs it. See section 3.
API_ORIGIN
none
The http address of the API, for a relative Http request and for /api/, for example http://api or http://127.0.0.1:8080. With no port, the server uses port 80. A / at the end is optional. A path, or https, is not valid.
BODY_SIZE_LIMIT
10M
The bytes of the body of one request. A number can end with K, M or G. A larger body gets status 413. All bodies in memory at one time have 16 times this limit, and a request above that gets status 503.
API_SIZE_LIMIT
64M
The bytes of one answer of the API. A larger answer is an error of the Http call, or status 502 for /api/.
MAX_CONNECTIONS
1024
The open connections. A new connection above this limit gets status 503. The limit of open files of the system must be higher.
REQUEST_TIMEOUT
30
The seconds that a client has to send one request, and the seconds of one write. A slower client gets status 408.
SURF_DEV
none
surf dev sets it. The server then marks the answer of each page with the header x-surf-page, so surf dev serves its own page for a page, and the answer of an endpoint as it is. Do not set it in a deployment.
Limit
Value
Result above the limit
The head of a request
64 KiB
Status 431
The headers of a request
100
Status 431
The wait of a connection for its next request
5 seconds
The connection closes
The wait of a page for its data
10 seconds
The page shows what it has
A connection, a read and a write to the API
30 seconds each
An error of the Http call
A picture on a native target
2^26 pixels
The picture does not paint
Request
Status
A path with no first /, a full address, or a #
400
A part . or .. in a path, also in escapes
400
A / or a \ in a part of a path: %2F, %5C or \, in each case of the letters
400
Two content-length values, or a value that is not digits
400
A space before the colon of a header, a line that does not end with CR and LF, or a control character in a header
400
HTTP/1.1 with no Host, or with two
400
transfer-encoding
501
Another version than HTTP/1.0 and HTTP/1.1
505
- Do TLS, and send Strict-Transport-Security.
- Limit the rate and the connections of each client. The server sees only the address of the proxy.
- Send a request body with a length. The server refuses a body in chunks.
- request.path in a hook has one form: /%61dmin/ reaches the hook as /admin, after a redirect. So compare it with ==. For a folder, compare the folder and the text under it: request.path == "/admin" || request.path.startsWith("/admin/").
- A path with %2F or %5C gets status 400. A rest parameter [...rest] joins its parts with /, so /files/a%2Fb and /files/a/b gave the page the same value. The guard of a folder saw only the second path. A route parameter therefore has no /. Put such a value into the query.
- A guard in the hook before also holds for the data of a page, because the hook reads the path of the page.
- A guard in a layout function holds for the page and for the data of the page. For a data request, a load function runs after each outer layout function, and the first failure stops it.
- A page render can start a load function with no parent together with the layout functions. The page does not show after a failure of a guard, but that load function can run. So a load function must not change data: use an action.
- A remote function has the path /__remote/<file>/<name>, not the path of a page. Check the session in the function, or in the hook.
- An endpoint of server.surfs and the forward of /api/ have their own paths. Guard these paths too. An API must check its own requests, because a client can reach it through /api/.
- A page with no server load function has no data on the server. A link in the browser shows it with no request. Its code is in the script of the page, so do not put a secret into a page.
- Store a random session id in a cookie, not a name. The server of examples/notes trusts a name, so that the guide stays short.
- A cookie of Wave.setCookie has HttpOnly and SameSite=Lax. HttpOnly protects the read of the cookie, not the calls of a script in the page.
- The server checks that a form comes from this site. An action must still check that the user can do the action.
- Check a redirect to a value of a user. The server refuses a line end, but https://evil.example and //evil.example are valid addresses. Accept only a path of your site:
- surffn safePath(url: String) -> String { return url.startsWith("/") && !url.startsWith("//") && !url.contains("\\") ? url : "/";}
- Check a value before it goes into a URL of Http. The server refuses a control character and a step .., and a route parameter has no /. But ? and # in a route parameter, and / in a value of the query, change the address. Use a matcher such as [id=int] or [id=uuid], or a matcher of the project.
- Do not give an address from a user to Http on the server. The server can reach hosts that the user cannot reach.
- A relative URL of Http goes to API_ORIGIN with the cookies and the authorization header of the client. An absolute URL goes out with neither.
- The server has no https: client. An absolute URL must use http:, so use it only inside your own network.
- href and action take a relative URL, http, https, mailto and tel. The element gets no attribute for another scheme.
- The value of each load function reaches the browser, also a value of load.server.surfs. Return only what the page shows.
- Wave.env in a file that the browser also reads needs a name in quotes that starts with PUBLIC_. Read a secret in a file of the server: *.server.surfs, server.surfs, actions.surfs, *.remote.surfs, or a file in a server folder.
- Do not commit a key. .gitignore of surf new names .env, *.pem and *.key.
- surf dev listens on 127.0.0.1. It answers only a Host of 127.0.0.1, localhost or [::1] with its port. It serves only the files in src/assets.
- surf dev and the inspector refuse a request of a page of another origin with status 403. A browser sends no Origin for <script src>, so the server reads Sec-Fetch-Site: only same-origin and none pass. A page on localhost with another port is same-site, so it does not pass. A link of another site can open a page of the app in a window, for example the redirect of a sign-in service. That site cannot read the window. A frame of another site cannot open a page. The inspector opens no page for a link of another site.
- Each response of surf dev and of the inspector has Cross-Origin-Resource-Policy: same-origin. A browser with no Sec-Fetch-Site then does not give the response to a page of another origin.
- surf build --single-file and the playground put the script and the styles into the page. < before /script or !-- becomes \x3C, and < before /style becomes <\, in each case of the letters. So a string of the app cannot end the script of the page.
- surf upgrade and scripts/install.sh check the SHA-256 of the archive with SHA256SUMS of the same release. This check finds a broken download, not a changed release. The Rust toolchain and the runtime pack have their SHA-256 in surf.
- surf upgrade installs only a newer version, so a release server cannot give back an older version with a known fault.
- A download of surf from an https address does not follow a redirect to http.
- A new runtime pack removes the old versions in toolchain and runtime of SURF_HOME, and the build results in target. A folder with another name stays, and target stays when Cargo did not make it.
- surf install runs no code of a package, and it refuses a symbolic link. A native build compiles the .native.rs files of a package, so use a package that you trust.
- The editor extensions start surf from the settings of the user or from the PATH. A folder cannot choose this program.