Security
This page summarises what AbiData defends against, what it assumes, and what is left to you.
Who is trusted
| Party | Trust |
|---|---|
| You, the operator (host, volumes, secrets, reverse proxy) | Fully trusted. Anyone with the data volume can read every workspace |
| Platform administrators | Trusted to manage accounts and workspaces. They cannot read a workspace's data unless they are also a member of it |
| Workspace owners and builders | Trusted with that workspace's data. Builders write scripts, which run on the server |
| Editors and viewers | Trusted only with the data their role allows |
| Everyone else, the network, uploaded files, remote web services | Not trusted |
What AbiData defends against
- Guessing passwords. Passwords are hashed with Argon2id. After 5 failed sign-ins for an account from one address, further attempts are refused for 15 minutes (RATE_LIMITED). Passwords must have at least 10 characters.
- Forgotten passwords. A platform administrator can set a new password for another user from the Admin console (
POST /api/v1/admin/users/{id}/password). It is signed-in-session only: API keys are always refused, and an administrator cannot use it on their own account (they must give their current password instead). The reset ends all of that user's sessions, clears their sign-in lockout and is audited asuser.password_resetwith the actor and target, never the password. The administrator then knows the temporary password, so share it over a secure channel and ask the user to change it. If no administrator can sign in, the operator can runabidata user reset-passwordon the host: see Install. - Stolen or forged requests (CSRF). Every request that changes data must carry the session's CSRF token and an
Originheader equal toABIDATA_PUBLIC_URL. Session cookies areHttpOnly,SameSite, andSecureover HTTPS. - Sessions. The cookie holds a random identifier; the server stores only its hash. Sessions end after 12 hours idle or 7 days, and a new session is issued at sign-in.
- Stolen API keys. Keys are 256-bit random secrets; the server stores only a keyed hash and compares it in constant time. See API keys below.
- Reading another workspace. Every workspace request checks membership first. A workspace you do not belong to, or an object in a different workspace, answers "not found", the same as one that does not exist.
- Dangerous SQL. User and script SQL runs under a database authorizer: only the allowed statements on workspace tables, no
ATTACH, noPRAGMAexcept a few reads, no internal tables, no schema changes from scripts. See Queries. It also has time, row and memory limits. On PostgreSQL the same rules come from database roles instead. User SQL runs as a per-workspace login that can only read that workspace's tables, never as the server's own role. See PostgreSQL. - Hostile scripts. Lua runs in a sandbox with no file, process, network or module access except through reviewed functions. Time, string, pattern and memory limits stop runaway scripts. See Scripts.
- Server-side request forgery. Outbound requests go through the egress gateway: see below.
- Hostile uploads. Imports and exports are size-capped and parsed into a separate staging database. Exported CSV text is neutralised against spreadsheet formula injection. Downloads of job artifacts are served with a restrictive
Content-Security-Policy: sandboxheader. - Browser attacks. Responses carry a strict
Content-Security-Policy(scripts only from the app itself, images only from the app unless an administrator sets a map tile server, no framing, no plugins, no<base>override, forms submit only to the app),X-Content-Type-Options: nosniffandReferrer-Policy: same-origin. WhenABIDATA_PUBLIC_URLis anhttps://address on a real host name (notlocalhostor an IP address), responses also carryStrict-Transport-Security: max-age=31536000. - Tampered backups. Backups are signed. See Backup strategy.
What it does not defend against
- A malicious or compromised host, container runtime or volume.
- Someone holding a builder or owner role. A builder can run scripts that read and write every table in the workspace.
- Weak passwords chosen by users above the minimum, and users sharing accounts.
- Transport security. TLS is provided by your reverse proxy. Run AbiData over HTTPS only. Without it, cookies are not marked
Secure. - Denial of service from the network. Put rate limits at the proxy if the service is exposed to the internet.
- On PostgreSQL, people who can run SQL in one workspace can see the table and field names, never the data, of the other workspaces in the same database. See PostgreSQL.
Roles
Roles are per workspace: viewer, editor, builder, owner. The full table is in Members and roles. In short:
- Viewer reads. Editor also writes records.
- Builder designs objects, writes scripts, creates jobs, imports data and uses the SQL console. Treat this as a powerful role, since scripts and SQL reach all of the workspace's tables.
- Owner also manages members and backups, including restore, which replaces all data.
- Platform administrator is a separate flag on the account. It allows creating workspaces and users, setting platform policy, and reading server logs and metrics.
Give people the lowest role that lets them do their work. Remove accounts of people who leave by disabling them in the admin area.
API keys
API keys let programs outside the browser use one workspace. Their rules are deliberately narrow:
- A key has the viewer, editor or builder role and never owner, so it can never manage members, backups, the egress allowlist, settings or other keys. The same role checks as for people apply to every route.
- A key is bound to one workspace. It works only on that workspace's
/api/v1/w/<id>/...routes; other workspaces answer "not found", and/api/v1/admin,/api/v1/auth,/metricsand the log stream refuse keys. - The secret is shown once. The database holds only HMAC-SHA256 of it under the server's session key (
session_key), so a leaked admin database yields no usable key. Changing the session key therefore also invalidates every API key as well as every session. - When a request has an
Authorization: Bearerheader, only that credential is considered and cookies are ignored. Such requests skip theOriginand CSRF checks because a browser never attaches that header by itself. The server sends no CORS headers and must keep it that way, or any website could try to use a key it obtained. - Each key is limited to 600 requests a minute. A client address that sends more than 30 wrong keys in a minute is refused further attempts for the rest of the minute; correct keys from that address keep working. Behind a proxy, set
ABIDATA_TRUSTED_PROXIESso the limit sees the real client address. - Writes are attributed to the key (
API key "name") in record history, audit entries andcreated_byfields. Creating and revoking keys is audited. - Keys belong to the workspace. Removing the owner who created one does not stop it. Owners revoke keys in the workspace settings; platform administrators can revoke any key in the admin console (API keys). A workspace holds at most 25 active keys.
- A key with the builder role can run scripts and SQL like a builder. Hand out builder keys only to code you trust as much as a builder.
Egress policy
Scripts can reach the internet only from jobs, only with fetch, and only to hosts on the platform allowlist. Only platform administrators edit it, in the admin console under Settings, Outbound requests. It is global: every workspace shares it, and owners cannot extend it.
- Each entry is a host (exact, or
*.domainfor subdomains), the ports allowed (default 443) and an optional note. There is no "any host" entry, and a wildcard must sit under a domain (*.example.com, not*.com). Public suffixes such as*.co.ukare not detected, so keep wildcards narrow. Write internationalised names in their ASCIIxn--form; other non-ASCII host names are refused. - A fresh install has an empty list, so no outbound request is possible until you add hosts (fail closed).
- Requests use https only. A switch allows plain
httpas well (the port must still be listed), and another switch turns all outbound requests off without losing the list. - Changes are written to the audit log (
egress.policy).
Regardless of the list:
- Addresses that are not public are refused: loopback, private ranges, link-local, carrier-grade NAT, documentation and test ranges, multicast and reserved space for IPv4, and anything outside global unicast for IPv6. This is checked on the address actually connected to, so a DNS answer that changes between lookups cannot slip through.
- Redirects are followed at most 5 times, and each hop is checked again. Credentials in headers are not forwarded to a different host.
- Responses are capped (2 MiB by default, 10 MiB at most), including after decompression.
- Each workspace is limited to 60 requests per minute, and every request is logged with host, status, size and duration.
See Jobs for what job authors see. Denied requests fail with EGRESS_DENIED.
File attachments
Uploaded files are untrusted content that people will open in their browsers, so AbiData never lets a file run as part of the app:
- The content type is detected from the file's bytes. The type or name the client sends is ignored, and a name only picks between harmless text and Office variants.
- Only PNG, JPEG, GIF and WebP images are shown in place (
Content-Disposition: inline). Everything else is an attachment. HTML, SVG, XML and other types a browser could run are served asapplication/octet-stream. - Every file response carries
X-Content-Type-Options: nosniff,Content-Security-Policy: sandbox; default-src 'none'; frame-ancestors 'none'andCross-Origin-Resource-Policy: same-origin. - Files are stored under their SHA-256, never under the client's name, so a name cannot reach the file system. Display names are stripped of paths, control characters and invisible direction marks.
- Image sizes are read from the header and images over 40 megapixels are refused before anything is decoded, so a decompression bomb cannot exhaust memory. Thumbnails decode at most two images at a time.
- Reading a file needs workspace membership (or a key for that workspace). A file id from another workspace is "not found", and a field can only hold ids of its own workspace.
- Uploading needs the editor role, is limited per file and per workspace, and counts against an API key's rate limit.
Files are not scanned for malware. If your people share files with each other outside AbiData, treat downloads like any other email attachment.
User blogs
User blogs publish user-written prose at /b/{username}:
- Public blogs are off on a fresh install until a platform administrator enables them in the admin console (
ABIDATA_PUBLIC_BLOGS=falselocks them off in the environment). - Posts are written in Markdown with raw HTML disabled, passed through an allow-list HTML sanitiser on every render, and served as server-rendered HTML with
Content-Security-Policy: default-src 'none'; style-src 'self'; img-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none',X-Content-Type-Options: nosniffandX-Frame-Options: DENY. No script, frame, form or external image can load. Inside the authenticated blog editor, the live preview renders in a sandboxed<iframe sandbox srcdoc>withoutallow-scripts. - Public blog routes (
/b/...and/api/v1/blogs/...) ignore session cookies andAuthorizationheaders, never reveal draft rows or counts, and return the sameNOT_FOUNDresponse whether a username does not exist, the user is disabled, the blog is off, or a post was taken down. - Turning a blog on or changing a post's status (
draft,published,unlisted) requires a signed-in owner session; API keys can write drafts but cannot publish. Status changes are limited to 10 per hour per user, and public reads are rate-limited per client address and per blog. - Outbound links carry
rel="nofollow noopener ugc". Visitors can report abusive pages without an account, and platform administrators can take down a post or an entire blog from the admin console's Blogs tab. - Setting
ABIDATA_BLOG_HOSTserves/b/...from a separate domain so browser session cookies forABIDATA_PUBLIC_URLnever reach public blog URLs: see Configuration.
Your checklist
- Serve only over HTTPS and set
ABIDATA_PUBLIC_URLto the HTTPS address. - Set
ABIDATA_TRUSTED_PROXIESto your proxy and nothing wider. - Keep
ABIDATA_DEBUG_ADDRoff, or bound to a private address. - Keep the secrets out of images and repositories. Use
_FILEvariables. Restrict file mode to 600. - Limit who holds the builder, owner and platform administrator roles, and review the workspaces' API keys from time to time (admin console, API keys).
- Keep the platform egress allowlist as narrow as you can: list the exact hosts your jobs need.
- If you enable user blogs, set
ABIDATA_BLOG_HOSTto a separate host name, review reported pages in the Blogs tab, and purge any proxy or CDN cache when taking content down. Leave public blogs off if nobody can moderate them. - Copy the backups and the keys off the host: see Backup strategy.
- On PostgreSQL, give the server's role only
LOGIN,CREATEROLEand ownership of its database, never superuser. Accept its connections only from the AbiData host, use TLS, and settemp_file_limit. See PostgreSQL. - Keep the image up to date: see Upgrade.