docs: security threat model and review findings
This commit is contained in:
1 parent
ce5ae6caa5
commit
5baf73f38b
2 files changed
+665
-82
No files matched your search
+209
-82
@@ -1,108 +1,235 @@
|
||||
# Security
|
||||
|
||||
## Core Controls
|
||||
AegisGitea-MCP is a full-access bridge into Gitea (ADR 0001): whoever can call it holds the
|
||||
power of the Gitea token it uses, which for the admin token means the whole instance. The
|
||||
server does not decide which operations are safe. It guarantees a small set of invariants
|
||||
(authentication off loopback, identity separation, audit, secret redaction, bounded and
|
||||
validated input and output, untrusted-content labelling) and leaves exposure to the owner
|
||||
(`READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS`, token scopes). The operator checklist is
|
||||
[hardening.md](hardening.md); open review findings are in
|
||||
[security-findings.md](security-findings.md).
|
||||
|
||||
- OAuth2/OIDC bearer-token authentication for MCP tool execution.
|
||||
- OIDC discovery + JWKS validation cache for JWT tokens.
|
||||
- Userinfo validation fallback for opaque OAuth tokens.
|
||||
- Scope enforcement:
|
||||
- `read:repository` for read tools.
|
||||
- `write:repository` for write tools.
|
||||
- Policy engine checks before tool execution.
|
||||
- Per-IP and per-token rate limiting.
|
||||
- Strict schema validation (`extra=forbid`).
|
||||
- Tamper-evident audit logging with hash chaining.
|
||||
- Secret sanitization for logs and tool output.
|
||||
- Production-safe error responses (no internal stack traces).
|
||||
## Threat model
|
||||
|
||||
## Threat Model
|
||||
### Assets
|
||||
|
||||
### Why shared bot tokens are dangerous
|
||||
| Asset | Where it lives | Impact if lost |
|
||||
|---|---|---|
|
||||
| Admin Gitea token (`GITEA_TOKEN`) | env file or container env; process memory | Full control of the instance, including Sudo as any user |
|
||||
| MCP bearer (`MCP_BEARER_TOKEN`) | env file; remote clients' configuration | Equivalent to the admin token over HTTP |
|
||||
| OAuth client secret (`GITEA_OAUTH_CLIENT_SECRET`) | env file | Impersonating this server towards Gitea's OAuth endpoints |
|
||||
| Per-user Gitea access and refresh tokens | `DATA_DIR/oauth-store.json` (encrypted), process memory | Acting as each signed-in user, for as long as Gitea accepts the refresh token |
|
||||
| Our issued `agm_` access and refresh tokens | clients; only SHA-256 hashes in the store | Acting as one user through this server until expiry or revocation |
|
||||
| Token store and its key (`TOKEN_ENCRYPTION_KEY`) | store in `DATA_DIR`, key in env | Store plus key discloses every user's Gitea tokens and every client secret |
|
||||
| Action secret values, webhook auth headers, minted credentials | in transit through tool calls | Credentials of CI, webhooks and other systems |
|
||||
| Audit log (`DATA_DIR/audit.log`) | `DATA_DIR` | Loss of accountability; integrity is the asset, not secrecy |
|
||||
|
||||
- A single leaked bot token can expose all repositories that bot can access.
|
||||
- Access is not naturally bounded per end user.
|
||||
- Blast radius is large and cross-tenant.
|
||||
### Actors
|
||||
|
||||
### Why token-in-URL is insecure
|
||||
- Owner: configures the server, holds the admin token. Trusted.
|
||||
- OAuth users: Gitea accounts that sign in through a client. Trusted with their own Gitea
|
||||
permissions only.
|
||||
- Remote MCP clients, including hosted ones (claude.ai, Mistral Le Chat): hold a bearer or an
|
||||
OAuth token and act through a model that reads untrusted content.
|
||||
- Malicious Gitea content authors: anyone who can write an issue, comment, file, commit
|
||||
message, release note or wiki page the model may read.
|
||||
- Network attackers: anyone who can reach the HTTP port or sit on the path to it or to Gitea.
|
||||
- Other tenants of the shared CI host: jobs and services on the same runner and Docker daemon.
|
||||
|
||||
- URLs can be captured by reverse proxy logs, browser history, referer headers, and monitoring pipelines.
|
||||
- Bearer tokens must be passed in `Authorization` headers only.
|
||||
### Trust boundaries
|
||||
|
||||
### Why per-user OAuth reduces lateral access
|
||||
1. MCP client to HTTP endpoint (`transports.py`): unauthenticated network input until a bearer
|
||||
or `agm_` token is verified (`auth.py`, `oauth/provider.py`).
|
||||
2. Browser to OAuth routes (`oauth/callback.py`, SDK `/authorize`, `/token`, `/register`,
|
||||
`/revoke`): unauthenticated, cross-site capable.
|
||||
3. Verified token to identity (`oauth/resolver.py`, `auth.py`): decides which Gitea token a
|
||||
call uses.
|
||||
4. Server to Gitea (`gitea/executor.py`, `oauth/upstream.py`): Gitea is trusted for
|
||||
authorization decisions, its content is not trusted.
|
||||
5. Tool result to model: everything Gitea returns crosses into a model context.
|
||||
6. Process to disk (`audit.py`, `oauth/store.py`, `config.py` reading `.env`).
|
||||
7. Repository to CI runner (`.gitea/workflows/*.yml`, `scripts/ci/*`): code under test runs
|
||||
with access to the runner's Docker daemon.
|
||||
|
||||
- Each MCP request executes with the signed-in user token.
|
||||
- Gitea authorization stays source-of-truth for repository visibility.
|
||||
- A compromised token is limited to that user�s permissions.
|
||||
### Threats and mitigations
|
||||
|
||||
## Resource-type-aware authorization
|
||||
Spoofing
|
||||
|
||||
The public server runs in *service-PAT mode*: a privileged bot token makes the
|
||||
actual Gitea calls while the per-user OAuth identity decides what the user may
|
||||
reach. Repository calls are gated by the user's collaborator permission on
|
||||
`owner/repo`. The rest of the Gitea surface — reachable through the
|
||||
`gitea_request` escape hatch — is gated by **resource-type-aware authorization**
|
||||
(`authz.py`). Every call is classified by `(method, path)` and enforced against
|
||||
a type-specific rule. **Every decision fails closed**: a call that cannot be
|
||||
classified, or whose permission cannot be positively verified against Gitea, is
|
||||
denied and audited.
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| Calling the endpoint without credentials | Non-loopback bind refuses to start without a bearer or OAuth; SDK `RequireAuthMiddleware` answers 401 | `transports.py` (`http_startup_errors`, `build_http_app`) |
|
||||
| Guessing the bearer | 32 character minimum; constant-time comparison of SHA-256 digests; per-IP and per-token rate limit before auth | `config.py`, `auth.py` (`StaticBearerVerifier`), `rate_limit.py` |
|
||||
| Using a token minted for another resource | RFC 8707 resource binding and required scope `gitea` enforced by the SDK; `/authorize` with a foreign `resource` gets `invalid_target` | `transports.py` (`_auth_settings`), `oauth/provider.py` |
|
||||
| Confused deputy through the shared Gitea OAuth app | Own consent page naming client and redirect URI, CSRF double submit with `SameSite=Strict` cookie, browser-binding cookie on the callback | `oauth/callback.py`, `oauth/provider.py` (residual: SEC-05) |
|
||||
| Code or refresh token replay | Single-use codes; replay or reuse of a rotated refresh token revokes the grant | `oauth/provider.py` |
|
||||
|
||||
| Resource type | Rule (service-PAT mode) |
|
||||
|---------------|--------------------------|
|
||||
| `repository` | Per-user collaborator permission on `owner/repo` (existing check). A repo path that cannot be parsed to `owner/repo` is denied. |
|
||||
| `org` | The signed-in user must be a **verified member** of the target org (checked against Gitea, fail closed). |
|
||||
| `user_owned` | A resource owned by a named user/org (`/users/{name}`, `/packages/{owner}`): allowed only when the owner is the caller, or the caller is a verified member of the owning org. |
|
||||
| `user_self` | Token-owner-scoped endpoints (`/user`, `/notifications`): **denied** — in service-PAT mode the data belongs to the bot, not the caller. |
|
||||
| `misc_global` | Instance-wide read-only utilities (markdown render, version, gitignore templates): reads allowed; writes denied. |
|
||||
| `admin` | **Default deny.** Allowed only when the operator opts in (`RAW_API_ALLOW_SENSITIVE=true`) **and** the signed-in user is a verified Gitea site administrator. |
|
||||
| `unknown` | Denied. |
|
||||
Tampering
|
||||
|
||||
This gate runs *in addition to* the policy engine and the `WRITE_MODE` gate — a
|
||||
write call is denied unless write mode is on, policy allows it, and the
|
||||
resource-type rule passes. In pure-OAuth mode (no service PAT) the user's own
|
||||
token already scopes every call at Gitea, so the extra gate is unnecessary.
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| Path traversal or path reshaping into other Gitea routes | Path values rejected on `..`, `.`, empty segment, `\`, `?`, `#`, `%`, control characters; percent-encoded with `safe=""` except verified wildcard routes | `gitea/encoding.py` |
|
||||
| Header injection (Sudo, multipart) | Sudo restricted to `[A-Za-z0-9_.-]{1,255}`; filenames and content types single line, no quotes; header parameters of the spec never forwarded | `gitea/encoding.py`, `catalog/build.py` |
|
||||
| Query smuggling of credentials or Sudo | `token`, `access_token`, `sudo`, `_sudo` refused as query names (any case) | `gitea/encoding.py` |
|
||||
| Malformed arguments | JSON Schema validation (Draft 2020-12, `additionalProperties: false`) before anything runs; messages never echo values | `server.py` |
|
||||
| Store tampering or wrong key | Fernet (authenticated encryption) with an HKDF-SHA256 key; undecryptable store stops startup instead of being wiped; symlinks refused | `oauth/store.py` (rollback residual: SEC-13) |
|
||||
| Settings injection from a planted `.env` | none yet | `config.py` (SEC-04) |
|
||||
|
||||
Positive verification results (org membership, site-admin) are cached briefly
|
||||
and bounded; only successful checks are cached, so a transient failure never
|
||||
grants access.
|
||||
Repudiation
|
||||
|
||||
## Full-API coverage: classified `gitea_request`
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| Unrecorded tool calls | Exactly one hash-chained audit entry per call; a call is refused when the audit log cannot be opened; startup fails on an unwritable log | `server.py`, `audit.py`, `transports.py` (write failures after startup: SEC-03) |
|
||||
| Undetected edits of the log | SHA-256 hash chain, `scripts/validate_audit_log.py` | `audit.py` (unkeyed chain: SEC-11) |
|
||||
|
||||
`gitea_request` exposes the long tail of the Gitea API that the curated typed
|
||||
tools do not cover, safely:
|
||||
Information disclosure
|
||||
|
||||
- **Deterministic read/write classifier.** `GET`/`HEAD` are reads; everything
|
||||
else is a write. A small, explicit override table may only *downgrade*
|
||||
provably side-effect-free render endpoints (markdown/markup) to reads — never
|
||||
the reverse — so a mutating call can never be misclassified as a read and slip
|
||||
past the `WRITE_MODE` gate.
|
||||
- **Known-path gate.** A request whose top path segment is not a recognized
|
||||
Gitea `/api/v1` route prefix is denied (fail closed): unknown paths are never
|
||||
passed straight through.
|
||||
- **Admin/credential denylist.** `/admin`, `*tokens*`, `*secrets*`, `*hooks*`,
|
||||
`*keys*`, `applications/oauth2`, and runner registration tokens are blocked for
|
||||
every method (including `GET`) and cannot be re-opened from `policy.yaml` —
|
||||
only `RAW_API_ALLOW_SENSITIVE=true` overrides them, and admin then still
|
||||
requires a verified site administrator (see above).
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| Tokens in results or errors | Exact redaction of the identity token, refresh token, admin token, bearer, client secret, store key and submitted secret fields in every result and error; pattern masking per `SECRET_DETECTION_MODE` | `gitea/executor.py`, `gitea/redaction.py`, `security.py` |
|
||||
| Tokens in logs | JSON log formatter without tracebacks; query strings dropped from access logs; upstream errors reduced to coarse kinds | `logging_utils.py`, `transports.py` (`AccessLogQueryFilter`), `oauth/upstream.py` |
|
||||
| Secret values echoed back | Submitted `password`, `secret`, `token` style fields and Action secret `data` redacted from the result | `gitea/encoding.py` (`sensitive_body_values`) (stored webhook auth headers on read: SEC-02) |
|
||||
| Leaks across identities | No cookie jar, no redirects followed, a fresh upstream client per OAuth exchange | `gitea/executor.py`, `oauth/upstream.py` |
|
||||
| Stack traces | `EXPOSE_ERROR_DETAILS=false` by default; even when on, only the exception class name | `server.py`, `gitea/executor.py` |
|
||||
|
||||
## Prompt Injection Hardening
|
||||
Denial of service
|
||||
|
||||
Repository content is treated as untrusted data.
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| Large request bodies | SDK body limit on the MCP endpoint and SDK OAuth routes; upload cap checked before decoding | `server.py` (`max_request_body_size`), `gitea/encoding.py` (consent route: SEC-01) |
|
||||
| Large or hostile responses | 16 MiB text cap, `MAX_BINARY_BYTES`, 64 KiB error bodies, JSON depth 100, item and character limits | `gitea/executor.py` |
|
||||
| Request floods | Sliding window per IP and per token hash, bounded key sets, rate-limit events audited once per window | `rate_limit.py`, `transports.py` (gaps: SEC-08) |
|
||||
| Unbounded OAuth state | Caps on clients (500), pending sign-ins (1024), codes (1024), sessions per login (50); store pruning | `oauth/provider.py`, `oauth/store.py` (flush by floods: SEC-09) |
|
||||
|
||||
- Tool outputs are bounded and sanitized.
|
||||
- No instructions from repository text are executed.
|
||||
- Text fields are size-limited before returning to LLM clients.
|
||||
Elevation of privilege
|
||||
|
||||
## Secret Detection
|
||||
| Threat | Mitigation | Implemented in |
|
||||
|---|---|---|
|
||||
| OAuth user reaching the admin token | Tokens with the OAuth marker only ever resolve to their session; only the static bearer (or no token on stdio and unauthenticated loopback) reaches admin-token mode; OAuth-only deployments fail closed | `oauth/resolver.py`, `auth.py` |
|
||||
| OAuth user using Sudo | Sudo is sent with the caller's own token; Gitea honours it only for site admins | `gitea/executor.py` |
|
||||
| Unknown Gitea scopes granting full access | Only granular `read:`/`write:` scopes accepted for `OAUTH_SCOPES` | `config.py` |
|
||||
| Model tricked by injected content | Untrusted-content labelling and annotations (see below); exposure narrowed by configuration | `server.py`, `gitea/executor.py` |
|
||||
| Untrusted code on the shared CI host | Per-run resource names and targeted cleanup | `.gitea/workflows/ci.yml`, `scripts/ci/cleanup_job_resources.sh` (fork PRs: SEC-06) |
|
||||
|
||||
Detected classes include:
|
||||
### Residual risks and owner decisions
|
||||
|
||||
- API key and token patterns.
|
||||
- JWT-like tokens.
|
||||
- Private key block markers.
|
||||
- Common provider credential formats.
|
||||
- Full access by default (ADR 0001). A leaked bearer equals the admin token. Sudo, admin,
|
||||
secrets and delete tools are enabled unless the owner narrows them.
|
||||
- Unauthenticated loopback mode is allowed for development. Any local user or process on the
|
||||
same host then acts with the admin token. Set a bearer on shared hosts.
|
||||
- Hosted clients (claude.ai, Le Chat) hold the bearer or an OAuth refresh token outside the
|
||||
owner's control. Prefer per-user OAuth and narrow `OAUTH_SCOPES`; the default grants
|
||||
`write:` on every category, including admin for site admins.
|
||||
- Credential-issuing operations (`gitea_user_create_token`, runner registration tokens such as
|
||||
`gitea_admin_create_runner_registration_token`, OAuth2 application secrets,
|
||||
`gitea_repo_create_deploy_token`) return the new credential to the model by design; only the
|
||||
caller's own tokens are still redacted.
|
||||
- Secret pattern masking is heuristic. Content containing third-party credentials (for example a
|
||||
Gitea token committed to a file) is returned unless it matches a pattern.
|
||||
- SSRF through Gitea features (migrations, webhooks, mirrors) is Gitea's responsibility; this
|
||||
server only ever contacts `GITEA_URL`.
|
||||
- The consent page is the only barrier against phishing with a self-registered client
|
||||
(SEC-05).
|
||||
- Key rotation for `TOKEN_ENCRYPTION_KEY` is not supported: a new key means moving the store
|
||||
away and signing every user out.
|
||||
- The audit chain is tamper-evident, not tamper-proof: anyone with write access to the file can
|
||||
rewrite it consistently. Ship it off the host.
|
||||
|
||||
Behavior:
|
||||
## Prompt injection
|
||||
|
||||
- `SECRET_DETECTION_MODE=mask`: redact in place.
|
||||
- `SECRET_DETECTION_MODE=block`: replace secret-bearing values.
|
||||
- `SECRET_DETECTION_MODE=off`: disable sanitization (not recommended).
|
||||
Everything returned from Gitea is untrusted data. Every success envelope carries the fixed
|
||||
`untrusted_content` notice, the server instructions tell the model never to follow
|
||||
instructions found in Gitea content, and the server itself never interprets content (it does
|
||||
not follow links, render, execute or chain calls). Tool annotations mark read-only and
|
||||
destructive operations so clients can require confirmation. The realistic attack is a model
|
||||
that reads a hostile issue and then calls a destructive tool; the defences are client-side
|
||||
confirmation, `READ_ONLY`, `TOOLSETS`, `DISABLED_TOOLS` and narrow token scopes
|
||||
([hardening.md](hardening.md)).
|
||||
|
||||
## Secret hygiene guarantees
|
||||
|
||||
- Settings hold secrets as `SecretStr`; `Identity`, `SessionRecord`, `UpstreamTokens` and the
|
||||
pending authorization exclude tokens from `repr`.
|
||||
- Results and errors: exact redaction of every known secret (identity token and refresh token,
|
||||
`GITEA_TOKEN`, `MCP_BEARER_TOKEN`, `GITEA_OAUTH_CLIENT_SECRET`, `TOKEN_ENCRYPTION_KEY`,
|
||||
secret fields submitted in the request), then pattern masking or blocking.
|
||||
- Validation errors and argument errors never contain argument values.
|
||||
- Audit entries carry argument names, never values, and are masked again before writing.
|
||||
- Logs: JSON lines, exception type only, access-log query strings replaced by `[redacted]`.
|
||||
- Our own tokens are stored only as SHA-256 hashes; the store file is encrypted, mode 0600,
|
||||
written through `O_EXCL` temporary files and an atomic rename.
|
||||
- Action secrets: Gitea's list endpoints (`gitea_actions_repo_list_secrets`,
|
||||
`gitea_actions_org_list_secrets`) return names and descriptions only, and a submitted value
|
||||
(`gitea_actions_update_repo_secret` and siblings) is redacted from the result.
|
||||
- Known gap: a webhook's stored `authorization_header` is returned by hook read tools such as
|
||||
`gitea_hook_repo_list_hooks` (SEC-02).
|
||||
|
||||
## Transport guarantees
|
||||
|
||||
- Fail-closed bind: `MCP_HOST` defaults to `127.0.0.1`; any non-loopback bind without a bearer
|
||||
or a working OAuth setup exits with code 2. A half-configured OAuth setup also exits 2.
|
||||
- Constant-time bearer check on SHA-256 digests, so neither content nor length leaks.
|
||||
- DNS-rebinding protection on the MCP endpoint: `Host` must be a loopback name, the bind
|
||||
address, the `PUBLIC_BASE_URL` host or an `ALLOWED_HOSTS` entry (421 otherwise); a foreign
|
||||
`Origin` gets 403; POST must be `application/json`.
|
||||
- Rate limits: sliding window per client IP and per Authorization hash on the MCP path and the
|
||||
unauthenticated OAuth paths, before authentication. Client IPs come from `X-Forwarded-For`
|
||||
only for `FORWARDED_ALLOW_IPS`.
|
||||
- `PUBLIC_BASE_URL` must be https unless loopback. Gitea redirects are never followed.
|
||||
- stdio: stdout carries JSON-RPC only; all logging goes to stderr.
|
||||
|
||||
### Per-user OAuth design
|
||||
|
||||
The server is an OAuth 2.1 authorization server for MCP clients (SDK provider, dynamic client
|
||||
registration, PKCE S256 required) and a confidential PKCE client of Gitea. Flow: `/authorize`
|
||||
parks the request and shows our consent page (client name, redirect URI, warning). Approval
|
||||
needs the form CSRF token and the matching `SameSite=Strict` cookie and sets a
|
||||
browser-binding cookie (`__Host-` prefixed and `Secure` on https, `HttpOnly`). The Gitea
|
||||
callback is completed only in the browser holding that cookie; unknown, expired or replayed
|
||||
states never redirect. Pages send `Cache-Control: no-store`, `Referrer-Policy: no-referrer`,
|
||||
`X-Frame-Options: DENY` and a CSP with `frame-ancestors 'none'`. Redirect URIs must be https,
|
||||
or http on loopback, without credentials or fragments. Issued tokens are opaque (`agm_` plus 256
|
||||
random bits), hashed at rest, bound to the MCP resource; access tokens live 1 hour, refresh
|
||||
tokens 30 days with rotation. Revocation ends the whole grant.
|
||||
|
||||
## Supply chain
|
||||
|
||||
- Base images pinned by digest (`python:3.12-slim`, `ghcr.io/astral-sh/uv`), non-root runtime
|
||||
user, root-owned virtualenv, no build arguments carrying secrets, `.dockerignore` excludes
|
||||
`.env`.
|
||||
- Dependencies locked in `uv.lock` and installed with `uv sync --locked`.
|
||||
- `pip-audit` over the exported lock (`scripts/ci/pip_audit.sh`, waivers in
|
||||
`security/pip-audit-ignore.txt`).
|
||||
- gitleaks pinned to 8.30.1 with sha256 verification, full-history scan with
|
||||
`.gitleaks.toml`.
|
||||
- ruff `S` (bandit) rules enabled in the normal lint and run separately in the `security` job.
|
||||
- Gaps: CI actions are pinned by tag, not commit SHA (SEC-07); no container image scan
|
||||
(SEC-16).
|
||||
|
||||
## Verification results
|
||||
|
||||
Run on 2026-10-14 against commit `ce5ae6c` (branch `feat/full-api-coverage`).
|
||||
|
||||
| Check | Command | Result |
|
||||
|---|---|---|
|
||||
| Security test suite | `uv run pytest tests/security --no-cov` | Pending: written in S2 (`tests/security/` does not exist yet, so the CI `security` job currently fails, SEC-15). Required tests are listed in [security-findings.md](security-findings.md). |
|
||||
| Dependency audit | `bash scripts/ci/pip_audit.sh` (pip-audit 2.10.1, `--strict`) | No known vulnerabilities in 83 locked packages (runtime and dev groups). No ignores in use. |
|
||||
| Bandit rules | `uv run ruff check --select S .` (ruff 0.17.0) | All checks passed, with the inline waivers listed below. |
|
||||
| Secret scan | `scripts/ci/install_gitleaks.sh` into a scratch directory, then `gitleaks git --config .gitleaks.toml --redact --no-banner --verbose --exit-code 1 .` (gitleaks 8.30.1) | No leaks found. 131 commits in history (103 non-merge); gitleaks scanned 109 commits with content, about 6 MB. |
|
||||
| Unit suite (context) | `uv run pytest` | 846 passed, 23 skipped (live Gitea and Docker tests), coverage 98.2 percent. |
|
||||
|
||||
## Waivers
|
||||
|
||||
Accepted findings, each with its reason. A new waiver needs a reason here in the same
|
||||
changeset.
|
||||
|
||||
| Id | Finding | Where | Reason |
|
||||
|---|---|---|---|
|
||||
| W-01 | S104 (bind to all interfaces) | `transports.py` `_WILDCARD_HOSTS`, `tests/test_server_helpers.py`, `tests/test_config.py` | The addresses are compared or used as test input, never bound. The container binds 0.0.0.0 through `MCP_HOST`, which requires authentication. |
|
||||
| W-02 | S105/S106 (hardcoded password) | `auth.py` (`token="static"`), `oauth/provider.py` (`TOKEN_PREFIX`, `token_type="Bearer"`), `audit.py` (mode string), `setup_snippets.py` (`TOKEN_PLACEHOLDER`) | Placeholders, public prefixes and type names, not secrets. |
|
||||
| W-03 | S101 (assert) | `setup_cmd.py` lines 357 and 822 | Type narrowing after an explicit check; behaviour does not depend on the assert. |
|
||||
| W-04 | S101, S105, S106, S108 in `tests/**`; S603, S607 in `tests/integration/harness.py` and `tests/mcp/helpers.py` | `pyproject.toml` per-file ignores, inline noqa | Test fixtures use fake tokens and temporary paths; the harness drives the trusted local Docker CLI with fixed argv. |
|
||||
| W-05 | gitleaks allowlists | `.gitleaks.toml` | Only `generic-api-key` and `curl-auth-header`, only in `tests/`, `docs/`, removed legacy guides and one removed source file; every match was reviewed as a fake or placeholder. |
|
||||
| W-06 | pip-audit | `security/pip-audit-ignore.txt` | Empty: no advisory is ignored. |
|
||||
| W-07 | Credential-issuing operations bypass pattern masking | `gitea/executor.py` (`CREDENTIAL_ISSUING_OPERATIONS`) | Their purpose is to return a new credential; exact redaction of the caller's own tokens still applies. |
|
||||
| W-08 | Unauthenticated `/health` and `/` report version and tool count, `/health` reports Gitea reachability | `transports.py` | Needed by container health checks and clients; reveals no configuration or secret. The Gitea probe is cached for 10 seconds. |
|
||||
| W-09 | Pattern masking keeps the first and last 4 characters of a matched secret | `security.py` (`mask_secret`) | Applies only to pattern matches, which are at least 16 characters; the remainder keeps enough entropy, and the context helps users recognise the value. |
|
||||
Reference in new issue
Block a user