Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Security

A website runs code next to the data of the account. The server is built so that the code you write is safe by default, and so that a mistake opens as little as it can.

What the server does

Risk What the server does
A script slipped into a page (XSS) Every value is escaped where it is written, in a text and in an attribute. javascript: and vbscript: addresses are refused when a file is saved, and when a value gives one. HTML which is not escaped is asked for by name : raw(), dangerouslySetInnerHTML. The text of a <script> or a <style> can not end its element.
Scripts of others A Content Security Policy on every page : the scripts of the website, of the UI libraries the page loads, and the ones with the nonce of the answer (a new one each time). Every <script> a page writes gets the nonce by itself ; attributes such as onclick do not run.
A post in the name of a visitor (CSRF) Every <form method="post"> gets the CSRF token of its visitor ; a POST, PUT, PATCH or DELETE without a valid token is refused (403) before any code runs. The cookies of a website are SameSite=Lax.
A stolen or forged session A session is a cookie sealed with AES-256-GCM, under a key of the installation and of the website : it can not be read nor changed by the browser, and a session of one website is nothing to another. HttpOnly, Secure over HTTPS, for the path of the website. The cookies of API Maker are never seen by a page.
Reading what a website should not ctx.db reaches the tables site.config.ts lists, nothing else. A rule keeps the rows of each visitor ($user), checks a row read or changed by its id, and checks the tables deep and paths read on the way. Writes may be kept to create. See data.
More than the visitor may ctx.api calls the APIs as the visitor : their access, groups, tokens and hooks. ctx.sys is off unless the website turns it on.
A page opened by whom it should not access of a page and of its layouts, read when the file is saved : a visitor who may not is refused before any code of the page runs. A setting which can not be read closes the page, it never opens it.
A redirect to another site (open redirect) redirect() refuses another host unless the code says { external: true } : a ?next= a visitor wrote never sends to another site.
A request to an inner server (SSRF) ctx.fetch reaches the hosts data.fetch lists only.
A page shown in a frame of another site (clickjacking) frame-ancestors 'self' and X-Frame-Options: SAMEORIGIN unless the website lists who may frame it.
A kept page shown to someone else A page is kept for everyone only when it says so and the visitor is anonymous ; never one which read cookies, headers, the address or the token of its visitor, set cookies, or showed one-time messages.
One website against another Each website runs in engines of its own, with a limit of memory (ssr.memoryMb) and of time (ssr.cpuMs, ssr.timeoutMs). The objects of JavaScript are frozen : a page can not change them for the pages after it. Nothing of one website is seen by another.
Errors which say too much A visitor reads the message of an error thrown on purpose, and a sentence for any other : the stack and the lines of your files are shown in the preview of the panel only.
The browser guessing X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Cross-Origin-Opener-Policy: same-origin, Permissions-Policy (no camera, microphone, geolocation, payment), Strict-Transport-Security over HTTPS.

The CSP

security.csp The policy
'strict' (default) default-src 'self' ; scripts of the website, of its libraries and with the nonce ; styles of the website, of its libraries and inline ; images from the website, data:, blob: and any HTTPS address ; fonts of the website and of its libraries ; requests (fetch) to the website ; forms post to the website ; no <object> ; frames of the same origin.
'relaxed' What HTTPS gives, inline scripts and eval included : for pages which load what they find.
'off' No policy.
any other text The policy itself, {nonce} written as the nonce of the answer.

A script of the page which needs the nonce in a value of its own (a library which adds scripts) finds it on its own <script> element : document.currentScript.nonce.

The panel and the websites

Serve the admin panel and the websites on different origins : another host, or another port. The panel of API Maker runs on a port of its own by default (adminPanel.port), so a website at /ssr/<apiPath>/<name> of the API port shares nothing with it. Behind a reverse proxy, give the panel and the websites different hosts : a website then can not read what the panel keeps in its browser.

What is left to you

  • Check every field a visitor sends, on the server : what a browser checks a request skips. Take the fields you name, never ctx.form as it came.
  • Never hand a value of a visitor to the database as a query : find: { category: String(ctx.query.category) }, not find: ctx.query. Escape the text of a regular expression.
  • Never give raw() what a visitor wrote.
  • Never write a value of ctx.user into a page kept for everyone (cache.scope: 'public').
  • Keep data.allowSys off unless a page needs everything the account may do ; then check every value which reaches it.
  • List the tables a website needs, no more, with the rows of each visitor (where: { owner: '$user._id' }), and allow: ['create'] for a table visitors only add to.
  • Serve the websites over HTTPS (a reverse proxy with TLS) : the session cookie is Secure then, and browsers get Strict-Transport-Security.