Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c94e03f
security: keep API credentials out of curl argv
racribeiro May 10, 2026
b9e2108
security: require explicit inbound channel trust
racribeiro May 10, 2026
cf7437b
security: enforce shell policy for cron jobs
racribeiro May 10, 2026
081c0c2
chore: add local Docker build workflow
racribeiro May 10, 2026
1acb34b
docs: document security patch scope
racribeiro May 10, 2026
fc4c0d1
chore: default local gateway port to 3210
racribeiro May 10, 2026
a3583f1
chore: publish local gateway on host interfaces
racribeiro May 10, 2026
dd32159
chore: run config before local gateway start
racribeiro May 10, 2026
367d574
chore: use IPv4 loopback for gateway healthcheck
racribeiro May 10, 2026
e0054f4
docs: describe compose gateway workflow
racribeiro May 10, 2026
6245d48
chore: bind local workspace into compose services
racribeiro May 10, 2026
c8b4563
chore: add compose agent and status targets
racribeiro May 10, 2026
64369a6
chore: keep compose up from rerunning config
racribeiro May 10, 2026
c21c41d
chore: include git and shell env in runtime image
racribeiro May 10, 2026
908e163
chore: remove stale Docker expose port
racribeiro May 10, 2026
464a133
chore: bind compose config file from checkout
racribeiro May 10, 2026
94808ca
chore: make down include compose profiles
racribeiro May 10, 2026
62f6a48
chore: mount docker socket for sandbox runtime
racribeiro May 10, 2026
8924ad4
chore: add docker and make to runtime image
racribeiro May 10, 2026
e53db58
chore: grant sandbox access to docker socket
racribeiro May 10, 2026
461f777
chore: expand shell sandbox command set
racribeiro May 10, 2026
cd9be80
Merge remote-tracking branch 'origin/main' into review-pr-907
DonPrus May 28, 2026
c557248
fix: complete PR 907 security hardening
DonPrus May 28, 2026
53bfcc0
fix: harden curl header temp files
DonPrus May 28, 2026
944d596
fix: redact verbose SSE payload logs
DonPrus May 28, 2026
97966e9
fix: harden compose init configuration
DonPrus May 28, 2026
0eaa762
test: cover credentialed legacy curl fallbacks
DonPrus May 28, 2026
2765c28
fix: preserve curl resolve pinning for credentials
DonPrus May 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
zig-out/
zig-cache/
.zig-cache/
.zig-global-cache/
zig-pkg/
*.db
*.db-journal
Expand All @@ -14,11 +15,13 @@ zig-pkg/
*.bak
*.a
reference/
workspace/

result
.direnv/
.worktrees
config.signal.json
config.json

# IDE configurations
.idea/
Expand Down
31 changes: 26 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,39 @@ FROM --platform=$BUILDPLATFORM alpine:3.23 AS builder

ARG ZIG_VERSION=0.16.0

RUN apk add --no-cache bash curl musl-dev python3
RUN apk add --no-cache bash curl git musl-dev python3

WORKDIR /app
COPY .github/scripts/install-zig.sh .github/scripts/install-zig.sh
COPY build.zig build.zig.zon ./
COPY src/ src/
COPY vendor/sqlite3/ vendor/sqlite3/

RUN set -eu; \
fetch_vendor_dep() { \
repo="$1"; \
commit="$2"; \
target="vendor/${repo}"; \
tmp_dir="$(mktemp -d)"; \
archive_path="${tmp_dir}/${repo}.tar.gz"; \
curl -fsSL "https://github.com/nullclaw/${repo}/archive/${commit}.tar.gz" -o "${archive_path}"; \
tar -xzf "${archive_path}" -C "${tmp_dir}"; \
extracted_dir="$(find "${tmp_dir}" -mindepth 1 -maxdepth 1 -type d | head -n 1)"; \
rm -rf "${target}"; \
mkdir -p "${target}"; \
cp -a "${extracted_dir}/." "${target}/"; \
rm -rf "${tmp_dir}"; \
}; \
fetch_vendor_dep websocket 9151f70b27024a072ea04425b9e7609eb6b1386c; \
fetch_vendor_dep wasm3 8a29b0080f1f65dbbe8b8f8c4d8148480feff377; \
sed -i '/\.websocket = .{/,/ },/c\ .websocket = .{\n .path = "vendor/websocket",\n },' build.zig.zon; \
sed -i '/\.wasm3 = .{/,/ },/c\ .wasm3 = .{\n .path = "vendor/wasm3",\n },' build.zig.zon

RUN set -eu; \
mkdir -p /tmp/zig-path; \
GITHUB_PATH=/tmp/zig-path/path RUNNER_TEMP=/opt bash .github/scripts/install-zig.sh "${ZIG_VERSION}"; \
ln -sf "$(cat /tmp/zig-path/path)/zig" /usr/local/bin/zig; \
zig version
test "$(zig version)" = "0.16.0"

ARG TARGETARCH
ARG VERSION=dev
Expand Down Expand Up @@ -77,23 +97,24 @@ FROM alpine:3.23 AS release-base

LABEL org.opencontainers.image.source=https://github.com/nullclaw/nullclaw

RUN apk add --no-cache ca-certificates curl tzdata
RUN apk add --no-cache ca-certificates curl docker-cli git make bash jq yq python3 nodejs perl tzdata && \
ln -sf python3 /usr/bin/python

COPY --from=builder /app/zig-out/bin/nullclaw /usr/local/bin/nullclaw
COPY --from=config /nullclaw-data /nullclaw-data

ENV NULLCLAW_WORKSPACE=/nullclaw-data/workspace
ENV NULLCLAW_HOME=/nullclaw-data
ENV HOME=/nullclaw-data
ENV SHELL=/bin/sh
ENV NULLCLAW_GATEWAY_PORT=3000

WORKDIR /nullclaw-data
EXPOSE 3000
ENTRYPOINT ["nullclaw"]
CMD ["gateway", "--port", "3000", "--host", "::"]

# Optional autonomous mode (explicit opt-in):
# docker build --target release-root -t nullclaw:root .
# make build DOCKER_TARGET=release-root IMAGE=nullclaw:root
FROM release-base AS release-root
USER 0:0

Expand Down
53 changes: 53 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
COMPOSE ?= docker compose
BUILDX ?= docker buildx
IMAGE ?= nullclaw:local
DOCKER_TARGET ?= release
VERSION ?= dev
PROFILE ?= gateway
SERVICE ?= gateway
RUN_ARGS ?=
CONFIG_ARGS ?= --interactive
COMPOSE_BAKE ?= true
NULLCLAW_PORT ?= 3210
NULLCLAW_UID ?= $(shell id -u)
NULLCLAW_GID ?= $(shell id -g)

export COMPOSE_BAKE
export NULLCLAW_IMAGE := $(IMAGE)
export NULLCLAW_DOCKER_TARGET := $(DOCKER_TARGET)
export NULLCLAW_VERSION := $(VERSION)
export NULLCLAW_PORT
export NULLCLAW_UID
export NULLCLAW_GID

.PHONY: build config up down run agent status shell logs check-buildx

check-buildx:
$(BUILDX) version >/dev/null

build: check-buildx
$(COMPOSE) --profile $(PROFILE) build $(SERVICE)

config: check-buildx
$(COMPOSE) --profile agent run --rm agent onboard $(CONFIG_ARGS)

up: check-buildx
$(COMPOSE) --profile $(PROFILE) up -d --build $(SERVICE)

down:
$(COMPOSE) --profile agent --profile gateway down

run:
$(COMPOSE) --profile agent run --rm agent $(RUN_ARGS)

agent:
$(COMPOSE) --profile agent run --rm agent agent

status:
$(COMPOSE) --profile agent run --rm agent status

shell:
$(COMPOSE) --profile agent run --rm --entrypoint /bin/sh agent

logs:
$(COMPOSE) --profile $(PROFILE) logs -f $(SERVICE)
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,51 @@ Then:
nullclaw --help
```

### 3) Common commands
### 3) Run with Docker Compose

The repository includes a Makefile wrapper around Docker Compose for local
containerized runs.

```bash
make build
make config
make up
```

`make config` runs `nullclaw onboard --interactive` inside the agent container.
For non-interactive setup:

```bash
make config CONFIG_ARGS="--api-key sk-... --provider openrouter"
```

The compose gateway defaults to `NULLCLAW_PORT=3210`, binds inside the
container on `0.0.0.0`, and publishes on all host interfaces:

```bash
curl http://127.0.0.1:3210/health
curl http://<host-ip>:3210/health
```

Override the port when needed:

```bash
make up NULLCLAW_PORT=8080
```

Operational shortcuts:

```bash
make logs
make down
make shell
```

Because the compose gateway is published on `0.0.0.0`, keep pairing, webhook
secrets, allowlists, and host firewall rules configured before using it on an
untrusted network.

### 4) Common commands

```bash

Expand Down
150 changes: 150 additions & 0 deletions SECURITY-PATCH-PLAN-2026-05-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Security Patch Plan - 2026-05-10

## Scope

This plan covers four security findings:

1. Telegram webhook spoofing and missing webhook authentication.
2. Secret exposure through `curl` process argv.
3. Cron shell jobs bypassing shell tool security controls.
4. Inbound channels allowing all senders when `allow_from` is empty.

Affected areas include:

- `src/gateway.zig`
- `src/http_util.zig`
- `src/providers/**`
- `src/channels/telegram_api.zig`
- `src/channels/discord.zig`
- `src/channels/line.zig`
- `src/cron.zig`
- config examples, documentation, and tests

Keep the patch limited to these security fixes. Do not combine it with unrelated refactors, feature work, or Zig toolchain changes.

## 1. Telegram Webhook Authentication

Risk: forged Telegram webhook updates can drive the agent and tools when the gateway is public or tunneled.

Plan:

- Add config support for a Telegram webhook secret compatible with Telegram's `X-Telegram-Bot-Api-Secret-Token` header.
- Require the secret-token header on `/telegram` webhook POSTs before parsing or dispatching the request body.
- Reject missing or mismatched webhook secrets with an explicit unauthorized response.
- Change `telegramSenderAllowed` so an empty `allow_from` denies by default.
- Require explicit `"*"` as the only allow-all sender configuration.
- Keep webhook failures non-sensitive in logs and responses.

Tests:

- Missing Telegram secret-token header is rejected.
- Incorrect Telegram secret-token header is rejected.
- Correct Telegram secret-token header is accepted.
- Empty Telegram `allow_from` denies.
- Explicit `"*"` allows all senders.
- Forged sender or chat IDs do not bypass webhook secret validation.

## 2. Remove Secrets From Child Process Arguments

Risk: local users, process monitors, or container hosts can read API keys, bearer tokens, bot tokens, proxy credentials, and webhook URLs from process argv.

Plan:

- Audit shared HTTP helpers and all credentialed callers.
- Replace credentialed `curl` execution paths with `std.http.Client`.
- Ensure provider requests do not place `Authorization`, API keys, or bearer tokens in child argv.
- Stop passing Telegram bot tokens in URLs to child processes.
- If any non-secret curl helper remains, make it reject credential-bearing headers and sensitive URLs before spawning.
- Keep outbound URL validation secure-by-default, including HTTPS-only behavior where currently required.

Tests:

- Credential headers are rejected by any remaining child-process HTTP helper.
- OpenAI and Gemini request paths do not use child argv for bearer tokens.
- Telegram API calls do not pass bot tokens through child argv.
- Sensitive values are not included in command construction errors or logs.

If argv exposure cannot be directly unit tested for a specific path, add a short comment near the code explaining the coverage limitation and the integration coverage expected.

## 3. Cron Shell Job Security Enforcement

Risk: anyone who can create or update cron jobs can gain persistent shell execution outside the normal shell tool policy, sandbox, environment scrub, timeout, and output limits.

Plan:

- Route cron shell execution through the same `ShellTool` and `SecurityPolicy` path used by normal shell tool calls.
- Validate shell cron commands when jobs are created or updated.
- Revalidate commands at execution time so previously stored unsafe jobs cannot bypass policy after upgrade.
- Preserve existing agent-job behavior unless it depends on unsafe shell execution.
- If full `ShellTool` integration is too invasive for the first patch, restrict cron REST endpoints to agent jobs only until a separately audited admin shell mode exists.
- Apply the same timeout, output limits, sandbox behavior, and environment scrub used by the shell tool.

Tests:

- Disallowed cron shell command is rejected at creation.
- Disallowed cron shell command is rejected at update.
- Previously stored disallowed cron shell command cannot execute.
- Timeout and output limits apply to cron shell execution.
- Environment secrets are not inherited by cron shell jobs.
- Agent cron jobs continue to execute as expected.

## 4. Deny Empty Inbound Allowlists

Risk: empty `allow_from` values create open-bot behavior that conflicts with the repository's deny-by-default security posture.

Plan:

- Normalize inbound channel semantics:
- Empty `allow_from` denies all senders.
- Explicit `"*"` allows all senders.
- Exact configured sender IDs allow only matching senders.
- Apply this to Telegram gateway handling, Discord, and LINE.
- Update config examples and docs to show explicit sender allowlists.
- Document the behavior change as intentional and security-sensitive.

Tests:

- Discord empty `allow_from` denies.
- Discord explicit `"*"` allows all.
- Discord matching sender allows and non-matching sender denies.
- LINE empty `allow_from` denies.
- LINE explicit `"*"` allows all.
- LINE matching sender allows and non-matching sender denies.
- Telegram gateway empty `allow_from` denies.
- Telegram gateway explicit `"*"` allows all.
- Telegram gateway matching sender allows and non-matching sender denies.

## Config And Documentation

Plan:

- Add a Telegram webhook secret field to the relevant config schema and examples.
- Use neutral placeholders such as `"test-secret"` and `"user_a"` in tests and examples.
- Avoid generating secrets silently during normal config loading.
- Update docs and examples that imply an empty allowlist is safe or permissive.
- Clearly state that allow-all requires explicit `"*"`.

## Validation

Required validation after code changes:

```bash
zig build test --summary all
zig build -Doptimize=ReleaseSmall
```

Security-sensitive changes should also include targeted tests for the modified modules before the full suite is run.

Environment note from 2026-05-10: the current system `zig` was observed as `0.14.1`, while this repository is pinned to `0.16.0`. Full validation must be performed with Zig `0.16.0`.

## Handoff Checklist

Before handing off or opening a PR, include:

1. What changed.
2. What did not change.
3. Threat notes for each fixed class.
4. Validation commands and results.
5. Remaining risks or unknowns.
6. Next recommended action.

1 change: 1 addition & 0 deletions config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"accounts": {
"main": {
"bot_token": "YOUR_TELEGRAM_BOT_TOKEN",
"webhook_secret": "replace-with-random-telegram-webhook-secret",
"allow_from": ["YOUR_TELEGRAM_USER_ID"],
"draft_previews": false
}
Expand Down
Loading
Loading