You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/deployment.md
+67-5Lines changed: 67 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -139,11 +139,13 @@ CHAT_DEPLOY=true # in .env (passed through to the container by compose)
139
139
With `CHAT_DEPLOY=true` the MCP **refuses `source_type="local"`** and returns a
140
140
message steering the caller to the safe inputs. What remains:
141
141
142
-
-**Git repos** — only `https://github.com/…` and `https://gitlab.com/…` are
143
-
accepted. The URL is checked twice (a literal `https://<host>/` prefix *and* a
144
-
parsed-hostname allowlist) and rejects other hosts, non-`https` schemes
145
-
(`git://`, `ssh://`, `file://`), embedded credentials, ports, and look-alike
146
-
domains — so a repo URL can't be turned into an SSRF probe.
142
+
-**Git repos** — `https://github.com/…` / `https://gitlab.com/…` are accepted,
143
+
plus `ssh://…` on hosts the operator listed in `GIT_CLONE_EXTRA_HOSTS`
144
+
(see [Custom git servers](#custom-git-servers-git_clone_) below). The URL is
145
+
checked twice (a literal `https://<host>/` prefix *and* a parsed-hostname
146
+
allowlist) and rejects other hosts, non-`https` schemes (`git://`, `file://`),
147
+
embedded credentials, ports, and look-alike domains — so a repo URL can't be
148
+
turned into an SSRF probe.
147
149
-**Pasted snippets** — `source_type="snippet"` with the code in a
148
150
`<code language="…">` tag; the language is validated/inferred and a mislabeled
149
151
or ambiguous snippet is refused. Nothing touches the host filesystem.
@@ -205,6 +207,66 @@ defaults — note a few differ in the shipped `docker-compose.yml` (called out b
205
207
|`ALLOWED_SOURCE_ROOTS`| `` (empty) |`:`-separated allowlist of dirs local sources must canonically resolve within (as the MCP sees them, e.g. `/app/playground`). Empty = no allowlist. |
206
208
|`GITHUB_TOKEN`| `` (empty) | PAT for cloning private repos (never embed it in the URL). |
207
209
210
+
### Custom git servers (`GIT_CLONE_*`)
211
+
212
+
By default `generate_cpg` only clones from `https://github.com/…` /
213
+
`https://gitlab.com/…`. To also analyze code on your own git server (e.g. a
214
+
self-hosted **Forgejo**/**Gitea** on the LAN), allowlist it with
215
+
`GIT_CLONE_EXTRA_HOSTS` — no other change is needed; callers then pass the
216
+
repo URL (`ssh://git@192.168.152.14:3000/<owner>/<repo>.git`) as
217
+
`source_path` with `source_type='github'`. Custom hosts are **ssh-only** —
218
+
http(s) clone URLs are rejected for them.
219
+
220
+
```bash
221
+
# ','-separated host[:port] entries — the same in both modes below.
222
+
# A bare host means port 22; `host:port` pins that port; `host:*` allows any.
223
+
GIT_CLONE_EXTRA_HOSTS=192.168.152.14:3000
224
+
225
+
# MCP run on the host (`python main.py`): key FILE on this host
226
+
GIT_CLONE_SSH_KEY_PATH=/abs/path/to/id_ed25519
227
+
228
+
# Full docker stack (`./scripts/deploy.sh`): HOST DIRECTORY containing the key
229
+
# as id_ed25519; compose mounts it read-only at /keys in the codebadger-mcp
230
+
# container, so the server sees /keys/id_ed25519
231
+
GIT_CLONE_SSH_KEYS_HOST_DIR=/abs/path/to/keydir
232
+
```
233
+
234
+
| Variable | Default | Description |
235
+
|---|---|---|
236
+
|`GIT_CLONE_EXTRA_HOSTS`| `` (empty) | ','-separated `host[:port]` entries accepted in addition to github.com/gitlab.com (IPv6 goes in brackets, e.g. `[::1]:2222`). A bare host means **port 22 only**; `host:port` pins that port; `host:*` allows any port on it. Allowlisted hosts are cloned over `ssh://` only; the built-in hosts keep their strict https-only, default-port-only posture unless an operator explicitly lists one here (e.g. `github.com:22` would enable ssh for github.com). Parsed once at startup — a malformed value fails the boot rather than surfacing on the first ssh clone. |
237
+
|`GIT_CLONE_SSH_KEYS_HOST_DIR`| `` (empty) |**Dockerized stack only.** Host directory containing the private key (named `id_ed25519`); docker compose mounts it read-only at `/keys` in the `codebadger-mcp` container and the server resolves the key to the fixed in-container path `/keys/id_ed25519`. Unset, an empty dir is mounted and no key path is configured. |
238
+
|`GIT_CLONE_SSH_KEY_PATH`| `` (empty) |**Host-run MCP only — ignored by the dockerized stack** (a host path never resolves inside the `codebadger-mcp` container; do not set it in `.env`). Private key FILE for `ssh://` clones of a custom host. The clone also sets `-o BatchMode=yes`, so a missing key fails fast instead of hanging on a prompt. |
239
+
|`GIT_CLONE_SSH_KNOWN_HOSTS`| `` (empty) |`known_hosts` file pinning the custom servers' host keys (`-o StrictHostKeyChecking=yes`). Unset, the clone falls back to `accept-new`: the key is recorded on first contact, but **in the dockerized stack that record lives in the container and is lost on every recreate**, making it trust-on-first-use each deploy. Like `GIT_CLONE_SSH_COMMAND` this is an in-container path there — put a `known_hosts` in the mounted key dir and set `/keys/known_hosts`. |
240
+
|`GIT_CLONE_SSH_COMMAND`| `` (empty) | Full ssh command override (passed to git as `GIT_SSH_COMMAND` for the clone); takes precedence over both key settings. In the dockerized stack any key path it references must exist **inside the `codebadger-mcp` container** (e.g. `/keys/…`). |
241
+
242
+
Notes:
243
+
-**Where does the clone run?** In the MCP process, so every path must make
244
+
sense *there*: host-run MCP → host filesystem; full docker stack → inside
245
+
the `codebadger-mcp` container. The two key variables above exist because of
246
+
this split: `GIT_CLONE_SSH_KEY_PATH` is a host path for host-run MCP,
247
+
while `GIT_CLONE_SSH_KEYS_HOST_DIR` is the compose bridge that maps a host
248
+
key dir onto the fixed container path `/keys` (hence the container always
249
+
sees `/keys/id_ed25519`). For a host-run MCP any key file name works
250
+
(`ssh -i` doesn't care); in the dockerized stack the key **must** be named
251
+
`id_ed25519` because the in-container path is fixed — or bypass it with
252
+
`GIT_CLONE_SSH_COMMAND`.
253
+
- Embedded credentials in the `source_path` URL are always rejected. For
254
+
github.com/gitlab.com private repos pass the PAT via the `github_token`
255
+
argument; it is injected into the clone URL and stripped from `.git/config`
256
+
after the clone.
257
+
-`ssh://` URLs may carry a username (`git@…`) but not a password; keys/agent
258
+
do the auth. scp-style `git@host:path` URLs are not accepted — use
Copy file name to clipboardExpand all lines: docs/security.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -73,7 +73,7 @@ The numbered controls are the boundary checks; each is described below.
73
73
| # | Boundary | Control | Where |
74
74
|---|----------|---------|-------|
75
75
| ① | Tool input → MCP |**Allowlist/format validation of every parameter**: `source_type`, `language` (whitelist), `codebase_hash` (`^[a-f0-9]{16}$`), `github_token` & `branch` (anti URL-/arg-injection, e.g. blocks `--upload-pack`), snippet `code`/`filename`/label, regex `pattern` (length + ReDoS shapes). |`src/utils/validators.py`|
76
-
| ①a | Repo URL → clone (**SSRF/undefined-clone prevention**) |**Strict allowlist on remote repos**: only `https://github.com/` or `https://gitlab.com/` (incl. `www.`). Enforced by **two independent gates** — a literal, case-sensitive `https://<host>/` prefix match *and* a parsed-`hostname` allowlist — plus rejection of any non-`https` scheme (`git://`, `ssh://`, `file://`, …), embedded credentials (`user:tok@`), non-default ports, and whitespace/control chars. Blocks userinfo host-smuggling (`https://github.com@evil/…`), internal/metadata hosts, and look-alike domains. |`validators.py` (`validate_repo_url`) |
76
+
| ①a | Repo URL → clone (**SSRF/undefined-clone prevention**) | **Strict allowlist on remote repos**: only `https://github.com/` or `https://gitlab.com/` (incl. `www.`) by default. For the built-in hosts, enforced by **two independent gates** — a literal, case-sensitive `https://<host>/` prefix match *and* a parsed-`hostname` allowlist — plus rejection of any non-`https` scheme (`git://`, `file://`, …), embedded credentials (`user:tok@`), non-default ports, and whitespace/control chars. Blocks userinfo host-smuggling (`https://github.com@evil/…`), internal/metadata hosts, and look-alike domains. **Operator extension**: `GIT_CLONE_EXTRA_HOSTS` (env) explicitly adds `host[:port]` entries which may also be cloned over `ssh://`, gated by the parsed-`hostname` allowlist (the literal-prefix gate is https-only) plus an **exact port match** — a bare entry means port 22, so one allowlisted host is not a licence to reach every port on that machine; `host:*` opts into any. A username but no password is allowed; auth rides in `GIT_SSH_COMMAND`, never the URL. Everything else about the posture (exact-hostname match, no embedded credentials, control chars, ≥`/owner/repo` path) is unchanged; the config is parsed at startup so a typo fails the boot, and the injected `github_token` for github.com/gitlab.com is stripped from `.git/config` after the clone. | `validators.py` (`validate_repo_url`), `services/git_manager.py` |
77
77
| ①b | Snippet code → CPG |**Language validated *and* inferred.** Pasted code is supplied in `<code language="…">` tags (parsed by regex); the declared language must be supported, and a content-signal check **refuses an obviously mislabeled tag** or **ambiguous/undeclared** language — every refusal returns an actionable message rather than building a wrong-language CPG. |`validators.py` (`parse_snippet_blocks`, `validate_and_infer_snippet_language`) |
78
78
| ② | Source staging |**Path confinement + symlink-safe copy.** Local paths must be absolute, are rejected if they contain null bytes/control chars, then `realpath`-canonicalized (collapsing `..` and resolving symlinks *before* any check) and screened against a system-dir denylist (`/etc`, `/proc`, `/sys`, `/root`, …). An optional `ALLOWED_SOURCE_ROOTS` allowlist hard-contains local sources to named roots. Snapshot reads confined with `realpath`+prefix / `commonpath`; the copy never dereferences symlinks whose target escapes the source tree. |`validators.py` (`resolve_host_path`), `core_tools.py`|
79
79
| ②a | Deployment posture |**`CHAT_DEPLOY=true` disables `source_type='local'` entirely** so a chat-facing / multi-tenant MCP cannot read arbitrary host paths — callers must use an allowlisted repo URL or a pasted snippet. |`core_tools.py`, `config.py`|
0 commit comments