| Version | Supported |
|---|---|
| 0.1.x | Yes |
| Anything earlier | Not applicable — 0.1.0 is the first release |
Fixes are made on the current 0.1.x line. There are no long-term support branches.
Vikado is two pieces with very different security properties, and it is worth being blunt about both.
The editor is a static single-page app. There is no account system, no login, and no server-side project storage. Projects live in IndexedDB and imported media lives in OPFS, both scoped to the browser origin on the user's own machine. Nothing is uploaded while editing, and closing the tab does not send anything anywhere.
Implications:
- Browser storage is not encrypted. Anyone with access to the OS user profile can read the projects and media Vikado has stored. Use full-disk encryption if that matters to you.
- Clearing site data for the origin destroys projects and media irreversibly. There is no server-side copy to restore from.
- Auto-captions run Whisper in the browser. The audio never leaves the machine, but the model weights are downloaded from the Hugging Face hub on first use and cached by the browser. That download is the only third-party network request the editor makes, and it happens only if a user asks for auto-captions.
vikado-server implements no authentication, no authorization, and no rate limiting. Every
endpoint under /api/v1 is open to anyone who can open a TCP connection to it, and the
server binds 0.0.0.0. Concretely, any client that can reach the service can:
- create render jobs and upload files, up to
VIKADO_MAX_UPLOAD_BYTESper request; - occupy the render queue and consume all available CPU;
- fill the disk backing
VIKADO_DATA_DIR, subject only to the job TTL; - read the status of, download the output of, and delete any job whose id it knows.
Job ids are the only thing standing between one client and another client's export. They are UUIDv7 values — random enough not to be enumerated in bulk, but they are bearer tokens by accident rather than an authorization check, and their leading bits encode the creation timestamp. Anyone holding a job id has full control of that job.
CORS is deliberately permissive (Access-Control-Allow-Origin: *), so a page on any website,
loaded in a browser that can reach the service, can drive the API — including a service bound
to localhost. That is a design consequence of letting the editor be hosted separately from
the renderer, and another reason the service does not belong on a shared network unprotected.
Vikado's render service is designed to run on localhost, or on a trusted network behind an authenticating reverse proxy. Exposing it directly to the public internet is not a supported configuration, and no part of the code is written on the assumption that untrusted clients can reach it. See docs/SELF_HOSTING.md for a reverse proxy configuration with HTTP basic auth.
Exporting is the only time user content leaves the browser. The editor uploads the source files referenced by the project — not the whole media library — plus the project JSON, which includes text and subtitle content. The server renders, the client downloads the MP4, and the workspace is deleted when the export dialog closes — or, if the tab goes away first, by the TTL sweeper. If the render service is not yours, those files are on someone else's machine for the length of the TTL.
These are properties of the current code, not aspirations:
- Uploads cannot escape their job workspace. The multipart field name must be exactly 64
hexadecimal characters (the file's sha-256), or the request is rejected with
BAD_ASSET_KEY. Client-supplied filenames are never used as paths; the original name is carried in the project only as a display label. Job ids are parsed as UUIDs before use, so paths like/api/v1/jobs/../../etc/passwdare 404s. Both behaviours are covered by tests incrates/vikado-server/tests/api.rs. - Request bodies are bounded. A body limit (
VIKADO_MAX_UPLOAD_BYTES, default 2 GiB) applies to every/api/v1route, uploads and render submissions alike. - Render concurrency is capped. A semaphore (
VIKADO_MAX_CONCURRENT_RENDERS, default 1) limits how many ffmpeg processes run at once; further jobs queue. - Jobs expire. A sweeper runs every 10 minutes and deletes any job older than
VIKADO_JOB_TTL_HOURS(default 24), cancelling the render if it is still running. Job state is in memory only, and every leftover workspace is removed at startup, so a restart is a full reset. - Clients cannot inject ffmpeg command-line arguments. The request body is deserialized
into the typed project schema, and the project's
schemaVersionmust match the server's or the request is rejected withUNSUPPORTED_SCHEMA. Encoder settings come from a closed enum (draft/standard/high) rather than free-form strings, ffmpeg is spawned as an argv vector with no shell involved, and the filter graph is written to a file rather than interpolated into a command line. Text and subtitle content reaches ffmpeg through a generated ASS file, with backslashes escaped and braces neutralised so it cannot open an override block. What goes inside the graph is a separate question — see the gaps below. - Transcription is local. Auto-captions never upload audio.
- No authentication, authorization, rate limiting or quotas, as described above. Add them at the proxy layer.
- No per-client isolation. Any client that learns a job id gets that job's output.
- Project asset references are not validated as content hashes. The upload endpoint
validates the storage key, but the render endpoint resolves each input by joining
assets[].hashfrom the submitted project onto the job's asset directory without checking its shape. A crafted project can therefore name a path outside the job workspace, and the referenced file — if ffmpeg can decode it — ends up in an MP4 the submitter can download. Treat the ability to reach the API as equivalent to read access to any media file the server process can read. Running in the provided container limits that to the image plus/data; running the binary directly as your own user does not. - Style strings are copied into the generated files unchecked. A
#-prefixedcanvasBackgroundand a chroma-key colour are pasted into thefilter_complexscript after only the#is stripped; a text clip's font name is pasted into the ASS style line as-is. Neither can reach the command line — the graph is passed as a script file — but a crafted project can add syntax of its own to what ffmpeg and libass parse. - A malformed colour panics the render task. A colour shorter than six hex digits panics
the ASS emitter, which kills the task and leaves the job reporting
renderingwith no progress until the TTL sweeper collects it. The process survives and the render slot is released, but the client waits forever. - Error messages are verbose. A failed render reports the last 60 lines of ffmpeg's stderr and absolute server-side paths to the client. That is deliberate — it is what makes export failures debuggable — but it discloses details of the server's filesystem.
- No wall-clock render timeout. A job runs until it finishes, is deleted, or is swept at the TTL. The TTL is the only upper bound.
- No access logging. The server logs a startup line and swept jobs. If you need an audit trail, take it from the reverse proxy.
- ffmpeg is the real attack surface. The service exists to feed untrusted media files to ffmpeg's demuxers and decoders, which is historically where media-handling vulnerabilities live. Rebuild the image regularly to pick up Debian's ffmpeg updates, and do not run the service alongside data you would not hand to its clients.
- The provided image runs as root.
docker/Dockerfilesets noUSER, so the server and the ffmpeg processes it spawns run as uid 0 inside the container. Passing--userdrops that, butVIKADO_DATA_DIRmust then be writable by the uid you choose — otherwise job creation fails withJOB_CREATE_FAILED. - No supply-chain attestation. Releases are not signed and there are no published image digests to verify against yet.
- Bind the service to localhost, or publish it only on a trusted network.
- Put an authenticating reverse proxy in front of it if more than one person uses it, and terminate TLS there — several editor features (OPFS storage, screen and webcam recording) require a secure context anyway.
- Prefer the container. It carries little more than ffmpeg, the binary and the built SPA, so its filesystem is a much smaller blast radius than a workstation — though the process inside it still runs as root, as noted above.
- Give
VIKADO_DATA_DIRits own volume, and lowerVIKADO_JOB_TTL_HOURSto the smallest value your users tolerate — one or two hours is usually enough. - Keep the image current; the ffmpeg it ships is where security updates matter most.
Report security issues privately through GitHub's security advisory flow:
https://github.com/koneb71/vikado/security/advisories/new
Please do not open a public issue, pull request or discussion for a suspected vulnerability. Public reports expose users of self-hosted instances before a fix exists.
A useful report includes:
- the affected component (editor,
vikado-server,vikado-renderer) and the version or commit you tested; - how the instance was deployed — in particular whether it sat behind a proxy;
- reproduction steps, ideally a minimal project JSON or HTTP request;
- what an attacker gains, and the access they need to start.
We aim to acknowledge reports within a week, and will keep you updated as the fix progresses. If you would like credit in the release notes, say so in the report; if you would rather stay anonymous, that is fine too.
Findings that amount to "the render service has no authentication" are already documented above and are not vulnerabilities in themselves. Reports that show how that design causes harm in a configuration this policy describes as supported — localhost, or behind an authenticating proxy — very much are.