chore(security): add OSS Scanner build image and threat model

Anthropic's OSS Scanner builds .oss-scanner/Dockerfile from this repository
and audits the image offline. The image keeps the source, the Go module and
npm caches, the debug panel binary, Xray-core and the sidecars, so tests,
the xray gRPC e2e suite, the master+node e2e and a live panel all run with
the network disabled. threat_model.md lists the trust boundaries, entry
points and the severity rubric the scanner should report against.
This commit is contained in:
MHSanaei
2026-10-11 22:29:36 +02:00
parent ab1c509329
commit eb498a3c13
2 changed files with 176 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# Build environment for Anthropic's OSS Scanner (https://github.com/anthropics/oss-scanner).
# The scanner builds this with the repository as the context, then audits the image offline:
# everything the build, the tests and a reproducer need must be fetched here.
FROM node:26-trixie-slim AS node
FROM golang:1.27.2-trixie
ENV DEBIAN_FRONTEND=noninteractive \
CGO_ENABLED=1 \
CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \
GOTOOLCHAIN=local
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl git jq procps sqlite3 unzip \
&& rm -rf /var/lib/apt/lists/*
# Node 26 is required: `make gen` imports .ts files directly and needs its type stripping.
COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
&& node --version && npm --version
# scanner contract: the checkout lives inside the image, at /src
COPY . /src
WORKDIR /src
RUN go mod download && go mod verify
# The Go binary embeds the Vite build from internal/web/dist, so the frontend goes first.
# HUSKY=0 skips the git-hook install, which needs a .git directory the build context may lack.
RUN cd frontend && HUSKY=0 npm ci && npm run build
# Debug info and no inlining, so stack traces and debuggers map straight to the source.
RUN go build -gcflags=all="-N -l" -o /usr/local/bin/x-ui . \
&& ln -s /usr/local/bin/x-ui /src/x-ui
# Xray-core, the mtg-multi MTProto sidecar and the geo databases, exactly as the release image ships them.
RUN ./DockerInit.sh "$(dpkg --print-architecture)" \
&& ln -s "/src/build/bin/xray-linux-$(dpkg --print-architecture)" /usr/local/bin/xray \
&& xray version
# Turns on the xray gRPC end-to-end tests (internal/xray, internal/amneziawgnet) against the real core.
ENV XRAY_E2E_BINARY=/usr/local/bin/xray
# Baseline tests: also warms the build cache. A failure is logged, never fatal.
RUN go test -count=1 -timeout 30m ./... > /src/.oss-scanner-go-test.log 2>&1 \
|| echo "WARNING: go test reported failures, see /src/.oss-scanner-go-test.log"
RUN cd frontend && npx vitest run --project unit --project components > /src/.oss-scanner-vitest.log 2>&1 \
|| echo "WARNING: vitest reported failures, see /src/.oss-scanner-vitest.log"
# Reproducers and patches must build with the network gone.
ENV GOPROXY=off \
GOSUMDB=off \
GOFLAGS=-mod=readonly \
npm_config_offline=true
CMD ["bash"]
+117
View File
@@ -0,0 +1,117 @@
# 3x-ui threat model
3x-ui is a self-hosted web control panel for Xray-core. One Go binary stores
inbounds, clients and settings in SQLite (default) or PostgreSQL, renders an
Xray JSON config from that state, supervises Xray-core and the `mtg-multi`
MTProto sidecar as child processes, runs AmneziaWG and TUIC servers in-process,
and serves an embedded React SPA. It is deployed on internet-facing VPS hosts,
usually as root, often in censored regions where a compromise exposes the
people relying on the proxy. Read `docs/architecture.md` in the scanned revision
for the code map; do not assume package names stay fixed.
## Trust boundaries and attackers
Trusted: the panel administrator (one account, or an admin-scope API token).
The admin can already edit the full Xray config, run a panel update, import a
database and set file paths, so admin-to-root is by design and out of scope.
Untrusted, in decreasing order of importance:
1. **Unauthenticated network attacker** reaching the panel port (default 2053)
or the subscription port. Login (`POST /login`, 2FA, LDAP), CSRF/session
handling (`internal/web/session/`), the login rate limiter
(`controller/login_limiter.go`), the `checkAPIAuth` gate in
`controller/api.go` (session, Bearer token, mTLS client cert),
`/csrf-token`, PWA/static routes, and the WebSocket `/ws` upgrade.
2. **Subscription end users.** They know only their own `subId` and fetch
`{subPath}/{subId}`, the JSON/Clash/Mihomo variants and `hwid-status`
(`internal/sub/`). Each must see only their own clients; check subId
guessing, cross-client leakage, header injection, forwarded-header trust
(`internal/sub/forwarded_trust.go`) and the public subscription page.
3. **Limited API tokens.** `monitor` and `node-sync` scopes are restricted by
explicit allowlists in `controller/api.go` (`enforceTokenScope`). Any way
for such a token to reach a route outside its allowlist is a vulnerability.
4. **Master/node peers.** A master panel drives child nodes over HTTPS
(`internal/web/runtime/`, TLS modes verify/skip/pin/mtls). A compromised
or malicious node must not gain control of the master or of other nodes;
node responses (inbound lists, traffic, online clients, certificates,
`descendants`) are untrusted input on the master.
5. **Proxy traffic.** Packets reaching AmneziaWG (`internal/amneziawgnet/`),
TUIC (`internal/tuic/`) and the loopback SOCKS5 relays into Xray. A peer
must not escape its per-peer auth, reach another peer's traffic, or crash
the panel process.
6. **Chat-bot users.** Telegram and Discord bot messages and callback data
(`internal/web/service/tgbot/`, `discord/`). Non-admin chats may only see
their own client (bound via the subId invite token); admin commands must
be unreachable from them.
7. **Imported content** an admin may receive from a third party: share links
and subscription URLs parsed by the import paths (`FuzzParseLink`), and
geosite/geoip `.dat` files (`internal/xray/geodata/`).
Also in scope: stored or reflected XSS in the SPA or the subscription page
from client remarks, emails, inbound tags or any other value a non-admin can
influence; SQL injection through GORM raw queries; path traversal in file or
certificate handling; secrets leaking into logs, subscription output, API
responses for limited tokens, or node sync payloads.
## How to exercise it
- Checkout at `/src`; the panel binary is `/usr/local/bin/x-ui` (debug build),
Xray-core is `/usr/local/bin/xray`, and the sidecars and geo files are in
`/src/build/bin`.
- Go tests: `cd /src && go test -count=1 ./...`. Real-DB helpers live in
`internal/database/dbtest` (`dbtest.InitDB(t, path)`); `internal/sub`'s
`initSubDB(t)` is the template for subscription tests. `httptest` is used
for HTTP. `XRAY_E2E_BINARY` is set, so the xray gRPC end-to-end tests run.
- Master + node end to end:
`go build -o /tmp/x-ui . && XUI_NODE_E2E_BINARY=/tmp/x-ui go test -count=1 -v ./internal/nodee2e/`.
- Fuzz targets: `FuzzParseLink`, `FuzzDecodeCertPin` (`go test -fuzz=...`).
- Frontend: `cd /src/frontend && npx vitest run --project unit --project components`.
The Storybook browser project needs Chromium and is not provisioned.
- Running a live panel: `XUI_DB_FOLDER=/tmp/xui XUI_LOG_FOLDER=/tmp/xui
XUI_BIN_FOLDER=/src/build/bin x-ui` (creates `admin`/`admin` on first start;
`x-ui setting -h` changes port, base path and credentials).
- Baseline logs from the image build: `/src/.oss-scanner-go-test.log` and
`/src/.oss-scanner-vitest.log`. A built image does not prove the tests passed.
- PostgreSQL is not installed: tests gated on `XUI_TEST_PG_DSN` skip, so a
green run says nothing about the Postgres dialect.
## Severity
- **Critical:** unauthenticated remote code execution; authentication bypass
to admin; a limited token, subscription user, bot user or node obtaining
admin control; reading the database or admin credentials unauthenticated.
- **High:** cross-client disclosure of subscription configs or credentials
without knowing the target's subId; scope escape of a `monitor`/`node-sync`
token; stored XSS that a non-admin can plant in the admin panel; a
malicious node compromising its master; SQL injection reachable by a
non-admin; peer isolation bypass in AmneziaWG/TUIC.
- **Medium:** remotely triggerable crash or resource exhaustion of the panel
process without authentication (it takes down every proxy it supervises);
CSRF on a state-changing admin action; login rate-limit bypass; secret
leakage into logs.
- **Low / informational:** issues that need admin access, local shell access,
or an attacker already able to modify the database or binaries.
## Reports and patches
Name the scanned commit, the attacker's starting position from the list
above, the configuration needed (default or not), the entry route, and the
full path to impact. Give a deterministic reproducer, ideally a Go test in the
package that owns the bug, using the real DB helpers rather than mocks.
Patches should fix the root cause, follow `CLAUDE.md` in the repository
(comments at most two lines, stdlib `testing`, state-changing inbound/client
operations dispatched through `runtime.Runtime`), and include a regression
test that fails without the fix. Merge findings sharing one root cause.
## Out of scope
- Anything requiring the admin account or an admin-scope token, including
admin-supplied Xray config, custom geo files, panel update and DB import.
- Vulnerabilities inside Xray-core, mtg-multi, amneziawg-go or other
dependencies, unless 3x-ui itself makes them reachable in a way upstream
does not; report the 3x-ui path.
- `skip` TLS mode accepting any node certificate: it is an explicit,
documented opt-out.
- The `docs/` site, `tools/`, install scripts and CI workflows.
- Missing hardening headers or cookie flags with no demonstrated exploit.