GitBolt is alpha software. Only the latest build gets security fixes.
Please report vulnerabilities privately through GitHub's private vulnerability reporting on
this repository (Security › Report a vulnerability on GitBoltApp/gitbolt). Don't open a
public issue for a security problem.
A useful report says which build you ran, what an attacker needs (a crafted repository, a remote, a forge account, local access…), and the steps to reproduce it.
GitBolt is a desktop app: a Rust core that runs git and talks to forges, and a React UI in an
embedded Chromium (CEF). It has no server and no accounts of its own. What follows describes the
current code; each point names where to look.
- The UI runs in Chromium's multi-process sandbox. GitBolt asks the CEF runtime for
SandboxPolicy::Required(crates/gitbolt-app/src/lib.rs): if Chromium can't sandbox its processes, GitBolt refuses to start instead of running unsandboxed. - On Windows, Chromium sandboxes only when GitBolt runs as a DLL under CEF's
bootstrap.exe, which makes the sandbox broker (RunWinMainincrates/gitbolt-app/src/lib.rs,vendor/tauri-runtime-cef/GITBOLT-PATCH.md). The installers ship exactly that:GitBolt.exeis the bootstrap, from the same CEF build as the one linked, andGitBolt.dllthe app (scripts/package-windows.ps1checks the versions match). A release build without the broker refuses to start. A debug build stays onAuto: a plaingitbolt.exe, ascargo buildmakes it, runs unsandboxed and logs a warning. - On macOS, the helper apps in
GitBolt.app/Contents/Frameworksenter Chromium's Seatbelt sandbox through the framework'slibcef_sandbox.dylibbefore they load the framework itself. This works with the bundle ad-hoc signed (no Apple identity). GitBolt only runs from its bundle there, which always ships the library: a helper without it fails to start rather than run unsandboxed. - The
.deband Arch packages ship Chromium'schrome-sandboxhelper owned by root with the setuid bit (mode 4755). The packaging checks fail the build otherwise (scripts/check-deb.sh,scripts/package-arch.sh). For development builds, see docs/dev-setup.md. - Release builds ignore Chromium switches given on the command line, so the installed binary
can't be started with
--remote-debugging-portor--disable-web-security. DevTools are off in release builds (vendor/tauri-runtime-cef).
- GitHub and GitLab personal access tokens are kept in the system keyring, through the Secret
Service D-Bus API (GNOME Keyring, KWallet, KeePassXC…) via the
keyringcrate (crates/gitbolt-forge/src/tokens.rs). Each is an item in the default collection with the attributesservice=gitboltandusername=<profile id>/<host>. - When no Secret Service is available, or it refuses the token, the token goes to a fallback
file,
~/.local/share/gitbolt/forge-tokens: mode 0600 in a 0700 directory you own, written through a temporary file and a rename, never through a symbolic link. Settings › Accounts marks such an account "File — not secure". Once a keyring is available, GitBolt moves the token there the first time it uses the account after a restart, then deletes the file copy. - Tokens never go into the settings or profile files: a profile records the host, the user and where the token is, not the token.
- A token is sent only in the
Authorizationheader, and only to its own account's host. API requests follow no redirects. An image redirect is re-checked, and the token never goes to another origin (crates/gitbolt-forge/src/http.rs). - In memory a token is a
Secrettype that never prints or serializes its value. As a second line of defence, every log line, git error and message shown in the UI passes through a redaction filter (crates/gitbolt-core/src/redact.rs). The filter removes URL credentials, token query parameters,Authorizationheaders and known GitHub and GitLab token formats. - Removing an account (Settings › Accounts) deletes its token from the keyring and from the fallback file. It doesn't revoke the token on the forge.
gitandsshrun the GitBolt binary itself asGIT_ASKPASS/SSH_ASKPASS(crates/gitbolt-core/src/askpass.rs). It forwards the prompt to the running app over a Unix socket created for this session in$XDG_RUNTIME_DIR, or in a private 0700 folder in the temp directory when that variable isn't set. The socket is mode 0600 and only accepts a peer with your user id (SO_PEERCRED). Each request must also carry a random 128-bit session token, which is compared in constant time.- Only a network operation that you started can show a prompt. GitBolt's own background fetch
is denied at once and never prompts. It also sets
GCM_INTERACTIVE=never, so Git Credential Manager doesn't prompt either. - Every git command runs with
GIT_TERMINAL_PROMPT=0. Network commands run in a new session with no controlling terminal, so nothing can prompt on the terminal GitBolt was started from. sshgetsSSH_ASKPASS_REQUIRE=force: passphrase, PIN and host key questions all come to GitBolt's dialog.- GitBolt doesn't keep your answers. A git credential helper you configured may keep them, as it would in a terminal.
- Network commands also run with
-c protocol.ext.allow=never, which disables git'sext::transport.
MR/PR descriptions and comments, and Markdown files in File View and Diff View, are rendered from
untrusted text (ui/src/markdown/).
- Raw HTML is parsed, then sanitized with
rehype-sanitize, using GitHub's schema with these changes (sanitize.ts):script,style,iframe,object,embed,svg,math,noscript,template,textarea,selectandtitleare removed along with their contents;- no
classattribute, exceptlanguage-*on code, and nostyle,tabindex,accesskeyor form attributes; idandnameget auser-content-prefix;- links may only be
http,https,mailtoor relative.data:images are allowed only as base64 PNG, GIF, JPEG or WebP.
- A file's front matter is parsed as plain data (YAML's core schema: no custom tags, a capped alias expansion; or TOML), and its values render as text, never as Markdown or HTML. A block that doesn't parse shows as a code block.
- Issue references, the front matter table, and the marks a rendered diff adds, carry a random nonce made for each
render. A value the document writes itself doesn't match the nonce and is dropped, so a
document can't forge them (
render.tsx). - Links are rendered without an
href, so the webview never navigates. A click goes through the core, which opens onlyhttp,httpsandmailtolinks in your default browser (crates/gitbolt-core/src/links.rs). - The webview never loads a remote image: every
<img>shows adata:orblob:URL. The core fetches the bytes. Images on the forge's own hosts load automatically. Images on any other host wait until you click "Load image from<host>", and are then fetched over https without any token. See PRIVACY.md. - Mermaid diagrams render with
securityLevel: 'strict'and are shown as an SVG image, in which scripts don't run. - A Content Security Policy is the second layer. The UI reaches the Rust core through a
single IPC command (
crates/gitbolt-app/src/lib.rs), so a script that ran in the page would have the UI's full powers. The policy (app.security.cspincrates/gitbolt-app/tauri.conf.json) keeps one from running even if it got past the sanitizer:default-src 'self': nothing loads from outside the app's bundle;script-src 'self' 'wasm-unsafe-eval': only the bundle's scripts run, plus WebAssembly for the syntax highlighter (Shiki's Oniguruma engine). Nounsafe-eval, nounsafe-inline: an inline<script>or anonerror=attribute is blocked. Tauri adds the hashes of the bundle's own inline scripts (the first-paint theme script inui/index.html, and one per bundled script file);style-src 'self' 'unsafe-inline': the editor (Monaco) and the UI set inline styles. This is the policy's one loose part: injected CSS could restyle the page, but can't run code or load anything from outside;img-src 'self' data: blob:: the core hands over every remote image asdata:orblob:;media-src 'self' blob:: a video in rendered Markdown plays from ablob:URL of the bytes the core fetched;connect-src 'self' ipc: http://ipc.localhost: the app's own IPC only;object-src 'none',base-uri 'none',frame-ancestors 'none',form-action 'none'.
- The end-to-end tests run the UI under the same policy (sent by
vite preview,ui/vite.config.ts), and fail on any violation (ui/e2e/test.ts).ui/e2e/csp.spec.tschecks that a script injected into rendered Markdown never runs.
GitBolt runs git the way you would in a terminal, so a repository's hooks and config apply.
That is a trust boundary.
- Writes run hooks. A commit runs
pre-commit,commit-msgand the other commit hooks. Merge, rebase, checkout and push run theirs. GitBolt has no option to skip them. - Opening a repository runs no commit hooks, but it does run git in it. What runs:
- Read commands:
git worktree listandgit statusas soon as the tab opens, then, as you browse,git log,git diff,git show,git blameand the like. git reads the repository's own.git/configfor each of them, so a command it names can run:core.fsmonitorruns ongit status, as it does in a terminal and in every git client. - A background fetch, unless it's Off (Settings › Fetch › Background fetch interval: Off).
When the tab opens, GitBolt runs
git fetch --allat once if the last fetch is older than the interval (always, the first time), then once per interval.git fetchruns thereference-transactionhook when it updates refs, and then git's auto maintenance (git maintenance run --auto), which can runpre-auto-gc. - Diffs run with
--no-ext-diff --no-textconv, so external diff and textconv drivers don't run.
- Read commands:
- Ownership. git's
safe.directorycheck applies unchanged: a repository owned by another user doesn't open ("detected dubious ownership"), unless your own git configuration lists it insafe.directory. GitBolt never setssafe.directoryitself, and its own reads (throughgix) open such a repository with reduced trust only until git refuses it. - To open a repository you don't trust without running its fetch hooks, set the background fetch
interval to Off first.
git statusand the other reads still run, so itscore.fsmonitorstill applies: open it only if you'd also rungit statusin it.
GitBolt can download and install its own updates from GitHub Releases
(crates/gitbolt-core/src/updates/, crates/gitbolt-forge/src/updates.rs).
-
Checksums. A downloaded package is kept only if the same release's
SHA256SUMSlists it with the same SHA-256 digest. A mismatch, a packageSHA256SUMSdoesn't list, or a release withoutSHA256SUMSdeletes the download and says so; it is never run. Right before the install, the digest is checked again, so a file changed in the meantime is deleted too. -
Where downloads come from. The release list comes from
api.github.comwithout any token. A package is fetched only fromgithub.com/GitBoltApp/gitbolt/releases/download/, and a redirect is followed only to GitHub's asset storage (objects.githubusercontent.com,release-assets.githubusercontent.com), over https, through the same HTTP client (TLS, proxy, timeouts) as the forge API. The download goes to~/.cache/gitbolt/updates/(0700;%LOCALAPPDATA%\gitbolt\updateson Windows,~/Library/Caches/GitBolt/updateson macOS). -
What
SHA256SUMSproves. It catches a corrupted or swapped download, but it comes from the same release, so it only proves the file is the one published there. The packages aren't code-signed yet. For a stronger check, GitHub's build provenance attestations tie each package to the release workflow that built it; verify one by hand with the GitHub CLI:gh attestation verify GitBolt_0.3.0_amd64.deb --repo GitBoltApp/gitbolt
-
The install. GitBolt runs
pkexec apt install -y ./<package>.deborpkexec pacman -U --noconfirm <package>as an argument list (no shell), so your desktop's polkit prompt asks for the password; GitBolt never sees it and never usessudoitself. Ifpkexecis missing or you cancel, it shows thesudocommand to run yourself. On Windows it starts the installer (the NSIS setup, ormsiexec /ifor the MSI) and quits. On macOS it runs a fixed script as you (/bin/sh -c, the two paths as arguments, never spliced into it): it mounts the.dmgread-only and unbrowsed, copies its GitBolt.app withdittobeside the running one and swaps them by rename, never asking for an administrator password; where the app's folder isn't writable it shows the manual steps instead. GitBolt's downloads carry no quarantine flag (it isn't App Sandboxed and doesn't setLSFileQuarantineEnabled), so Gatekeeper doesn't check the copy again:SHA256SUMSis the check, as on the other platforms. -
Which package. Each package names how it was installed in an
install-kindfile beside the binary (deb,arch,nsisormsi), or in the bundle'sContents/Resources(dmg); a build from source has none and is only offered the release page.
- The end-to-end test harness (
crates/gitbolt-harness) is a separate binary. It serves the API over a WebSocket on127.0.0.1, with/test/*routes. It accepts WebSocket upgrades only fromlocalhost,127.0.0.1ortauri://origins, and refuses writes outside fixture repositories. The packages don't include it: they contain only thegitboltbinary and CEF's files. - Test-only API requests are compiled only with
gitbolt-core'stestingfeature, and only the harness turns it on. The packaged app is built fromcrates/gitbolt-appalone, which doesn't. Acargo build --workspacewould unify the feature intogitbolt-appas well, so a release build ofgitbolt-appwith it on fails to compile (a constant assertion incrates/gitbolt-app/src/lib.rs). Debug workspace builds, used for tests and lints, are unaffected. - The UI's test hooks (
window.__gband the minimized-window override) are compiled only into development and e2e builds. The WebSocket transport is used only when the page isn't running inside the app.