# Security This page describes what Openhub protects against, how, and where it is still exposed. It is meant for anyone deciding how to deploy Openhub, and for anyone changing code that handles untrusted input. ## Threat model **Trusted:** - The operator, who has shell access and owns the data directory. - The site administrator, who has admin rights on every repository. - Repository owners and collaborators, within their own repositories. **Untrusted:** - Anonymous visitors. - Signed-in users acting on repositories they don't belong to. - When registration is open, anyone on the internet, who can create an account and push arbitrary repositories of their own. **Untrusted input enters through:** - **HTTP requests:** forms, URLs, headers and smart HTTP bodies. - **SSH sessions:** authentication attempts, exec commands and pack data. - **Repository content.** Every object pushed by a user is later parsed by Openhub (libgit2, syntax highlighting, Markdown) and by `git` (`upload-pack`, `receive-pack`, `archive`). - **Markdown and text** in profiles, repository descriptions, pull requests and comments. ## Protections in place | Area | Protection | Where | | --- | --- | --- | | Passwords | Argon2id with a random salt per password. Verification runs even for unknown accounts, so timing doesn't reveal which accounts exist | `src/web/account.rs` | | Sessions | 256-bit random tokens. Only their SHA-256 is stored. `HttpOnly` and `SameSite=Lax` cookies, `Secure` under HTTPS. Other sessions are revoked when the password changes | `src/web/ctx.rs`, `src/web/account.rs` | | CSRF | `SameSite=Lax`, plus an `Origin` check on every non-GET request outside git HTTP | `ctx::cross_origin` | | Open redirects | `next` and `return_to` must be same-site paths | `web::safe_next` | | XSS in pages | askama escapes every value. `\|safe` is only used on HTML generated by Openhub. A strict CSP (`script-src 'self'`, no inline script, `object-src 'none'`, `base-uri 'self'`) applies | templates, `ctx::middleware` | | XSS through Markdown | Raw HTML is sanitised by ammonia after rendering. Links get `rel="noopener noreferrer nofollow"` | `src/markdown.rs` | | XSS through hosted files | Raw files are served as `text/plain` (or as an image, or as an attachment), with `nosniff` and `CSP: default-src 'none'; sandbox` | `code::raw` | | Clickjacking | `frame-ancestors 'none'` on HTML pages | `ctx::middleware` | | SSH command execution | Only `git-upload-pack` and `git-receive-pack` are accepted, through a strict parser that rejects quotes, backslashes and NUL bytes. git is spawned directly with no shell. The repository path comes from the database ID | `src/ssh.rs` | | SSH access | Public-key authentication only. A 300 ms delay after each rejection. No shell, pty or subsystems | `src/ssh.rs` | | Path traversal | No filesystem path is built from user input. Repository paths are `repos/.git`. File paths inside a repository are looked up in git trees, not on disk | `Config::repo_path`, `src/git.rs` | | Private repository privacy | Unreadable repositories return 404 on the web and "Repository not found" over SSH | `repo::load_repo`, `ssh.rs` | | Writing to repositories | Only SSH `receive-pack` (with write access) and web merges and branch deletions (with write access) change refs. Pushes can't write repository config or hooks, and no hooks are installed | `src/ssh.rs`, `src/web/pulls.rs`, `src/web/repo.rs` | | Openhub-managed refs | `receive.hideRefs=refs/pull` stops clients rewriting pull request heads | `git::configure` | | Merge races | The base branch is updated with a compare-and-swap, so a merge can't overwrite a concurrent push | `git::merge` | | Pasted private keys | The key form rejects input that contains `PRIVATE KEY` | `validate::ssh_public_key` | | Rendering cost | Highlighting stops at 512 KB. Files over 1 MB aren't displayed. Diffs stop at 3,000 lines per file and 20,000 in total. The last-commit search stops at 5,000 commits. Request bodies are limited to 2 MB (64 MB for `git-upload-pack`) | `src/highlight.rs`, `src/web/code.rs`, `src/git.rs`, `src/web/mod.rs` | | Container | The Docker image runs as an unprivileged `openhub` user, which owns only `/data` | `Dockerfile` | ## Known gaps The protections above suit an instance whose users are trusted. The gaps below matter most when registration is open, or when the server is reachable from the internet. ### Abuse and resource exhaustion 1. **Registration is open by default, and the first account becomes administrator.** Anyone who finds the URL can create an account, along with unlimited public and private repositories. If the server is reachable before the operator registers, a stranger can take the administrator account. 2. **There is no rate limiting** on sign-in, sign-up, SSH authentication or HTTP Basic authentication. Each password check costs about 19 MB of memory and noticeable CPU. Sign-in runs one even for unknown accounts, and private HTTP clones run one per request. A flood of attempts can exhaust memory or CPU without needing an account. 3. **There are no quotas.** Repository count, repository size and push size are all unlimited. 4. **git child processes have no timeouts or resource limits.** A slow or huge push or fetch holds a process for as long as the client keeps the connection open, up to the 1-hour SSH inactivity timeout. 5. **The SSH input queue is unbounded.** Data from the client is queued in memory faster than git can read it (see [Git transport](git-transport.md#authorisation-and-execution)), so a fast client can grow server memory. 6. **gzip request bodies to `git-upload-pack` are decompressed without a size limit.** The 64 MB limit applies to the compressed body only. 7. **Some page loads are expensive for large repositories.** The commit count on the repository home walks the full history. Pull request pages compute mergeability with an in-memory merge on every view. Pathological input can make syntax highlighting slow, within the 512 KB cap. ### Code execution Openhub's own code is Rust, so it is memory-safe. The real exposure is C code parsing repository data that users control: 8. **libgit2 runs inside the server process.** It parses pushed objects to show trees, diffs and merges. An attacker controls those objects, and can trigger merges of crafted content in their own repository. A memory-corruption bug in libgit2 (there have been such CVEs, for example CVE-2024-24577) would compromise the whole process: every repository, the password hashes and the SSH host key. Keep the `git2`/`libgit2-sys` dependency up to date. 9. **The `git` program parses untrusted packs** in `receive-pack` and `upload-pack`, and runs with the same filesystem access as Openhub. Keep the system `git` patched. 10. **`git archive` honours a repository's own `.gitattributes`, including `export-subst`.** That setting feeds attacker-controlled format strings into git's pretty-format engine, the route used by CVE-2022-41903 (heap overflow in `git archive`). A patched git closes that bug. The feature remains attack surface. 11. **`receive.fsckObjects` isn't enabled,** so malformed objects are accepted. Openhub can then store and serve objects crafted to attack the git *clients* of people who clone them, such as past `.gitmodules` and symlink exploits. Enabling it rejects some older real-world repositories with harmless defects. That's why it's off, but the trade-off is different for a public instance. ### Privacy and moderation 12. **An anonymous HTTP clone can tell a private repository from a missing one.** The private one gets 401, the missing one 404 (see [Accounts and access](accounts-and-access.md#private-repositories-look-like-missing-ones)). The web interface and SSH don't leak this. 13. **Images in Markdown can load from any HTTPS host** (`img-src https:`), so a README can show the IP addresses and user agents of its readers to a third party. 14. **There are no moderation tools.** There's no admin list of users, no way to disable or delete accounts in the interface, and comments can't be edited or deleted. An administrator can delete any repository. 15. **There's no email verification, password reset, two-factor authentication or session list.** Sessions aren't bound to an IP address or client. ## Hardening advice ### Private or team instance (registration off, trusted users) 1. **Register the administrator account first**, before the server is reachable from anywhere else. 2. **Then set `OPENHUB_DISABLE_REGISTRATION=true`.** Create later accounts by briefly re-enabling registration, or by having the administrator register them. 3. **Serve over HTTPS** behind a reverse proxy, and set `OPENHUB_PUBLIC_URL` to the `https://` URL. That also marks cookies `Secure`. 4. **Add rate limiting at the proxy** for `POST /login`, `POST /register` and `/*/*/info/refs`. 5. **Keep git, the base image and Rust dependencies updated:** use `apt upgrade` or rebuild the image, run `cargo update`, and rebuild. 6. **Harden the container:** ```yaml services: openhub: read_only: true cap_drop: [ALL] security_opt: ["no-new-privileges:true"] tmpfs: [/tmp] mem_limit: 1g pids_limit: 256 ``` ### Public instance with open registration Do everything above. In addition, before opening sign-ups, Openhub needs changes that don't exist yet. Roughly in priority order: 1. **Control who can sign up:** invite-only registration, or administrator approval. Add moderation: list, disable and delete users and content. 2. **Add limits:** - A push size limit (`receive.maxInputSize`). - Per-user repository count and disk quotas. - Rate limits on authentication and pushes inside Openhub. - A cap on concurrent password hashing. - A bounded SSH input queue, with timeouts on git processes. - A size limit on decompressed request bodies. 3. **Enable `receive.fsckObjects`,** with `receive.fsck.=ignore` exceptions for common harmless defects. 4. **Sandbox git child processes.** Use Landlock, seccomp, bubblewrap or nsjail so each process sees only its own repository (read-only for fetches and archives), has no network, and runs under CPU, memory and time limits. Alternatively, build archives with libgit2 so `export-subst` never runs. 5. **Move libgit2 work into a separate sandboxed worker process.** This is the largest change, and the only one that contains a libgit2 memory-safety bug. 6. **Return 401 for missing repositories over HTTP** (as GitHub does), so private and missing repositories look the same. 7. **Proxy external Markdown images**, or limit them to the Openhub origin. ## When you change this - **Any new handler that reads repository content** must go through `load_repo` (web) or `AppState::access` (other channels), and must keep private repositories indistinguishable from missing ones. - **Any new HTML built in Rust and marked `|safe`** must escape every user-controlled value. Add a unit test showing that `