Skip to content

fix: security and correctness fixes from #107 - #108

Open
stealthwhizz wants to merge 15 commits into
open-gitagent:mainfrom
stealthwhizz:bug/security-fixes
Open

stealthwhizz wants to merge 15 commits into
open-gitagent:mainfrom
stealthwhizz:bug/security-fixes

Conversation

@stealthwhizz

Copy link
Copy Markdown

Fixes #107

This PR fixes the security and correctness issues in #107 and upgrades the vulnerable dependencies.

Security

  • Knowledge paths: knowledge documents must resolve inside knowledge/, with symlinks resolved. A shared resolveInside helper is used by all 9 adapters. Claude runner hook scripts are confined to hooks/ the same way.
  • install: a mount must be a relative path inside the agent dir, and absolute paths, .. and . are refused. Dependency names stay inside .gitagent/deps. Local source: and extends: paths outside the agent dir need the new --allow-external-sources flag.
  • Lyzr user_id: the API key is never used as user_id. The order is --user-id, then LYZR_USER_ID, then a non-reversible id derived from a SHA-256 hash of the key.
  • registry: git and gh run through execFileSync with argv. Agent names must match ^[a-z][a-z0-9-]*$, and the repo URL is parsed and checked (https, github.com, owner/repo only).
  • Gemini runner: shell: true is removed. On Windows it calls cmd.exe with quoted arguments and refuses arguments that cmd.exe can't pass safely.
  • Credentials on disk: provisioned credential files are written with mode 0600 in 0700 directories. New entries are merged into the existing nanobot and OpenClaw config instead of overwriting it. A config file that can't be parsed stops the run.

Correctness

  • Claude runner:
    • Hook commands are shell-quoted.
    • System prompts over 100 KB go through --append-system-prompt-file, which avoids E2BIG.
    • Temp files are always cleaned up.
  • init and import: init -d and import create the target dir.
  • import:
    • Claude import copies skill subdirectories.
    • parseSections keeps the preamble and respects code fences.
    • The CrewAI importer accepts the standard format and sanitizes sub-agent names.
  • export -f claude-code: it no longer writes into the repo. Parent skills are merged in memory and referenced at .gitagent/parent/skills/....
  • OpenClaw runner: the config shape matches the adapter.
  • Skills: CRLF SKILL.md files load, and allowed-tools accepts a string or a list.
  • Knowledge index: an empty knowledge/index.yaml no longer crashes the adapters.
  • Smaller fixes:
    • The gitclaw runner copies hooks, knowledge and full skill dirs.
    • GitHub skill refs are validated.
    • No process.exit inside try/finally.
    • run -a git doesn't clone twice.
    • Git calls have -- before URLs.

Dependencies and CI

  • js-yaml 4.3.2, ajv 8.20.0, fast-uri 3.1.8. npm audit reports 0 vulnerabilities.
  • GitHub Actions are pinned to commit SHAs.

Behavior changes

  • install: local sources or extends outside the agent dir need --allow-external-sources. A mount that is absolute, escapes the agent dir or equals its root is refused.
  • Knowledge documents and hook scripts outside their directories are skipped with a warning.
  • The default Lyzr user_id changed, so old sessions keyed by the raw key won't match new ones.
  • registry refuses agent names that aren't lowercase kebab-case, and repo URLs with extra path segments, query strings or fragments.
  • Gemini on Windows refuses prompt or model values that contain ", %, ! or a line break.
  • export -f claude-code points inherited skills at .gitagent/parent/skills/..., so a fresh clone needs opengap install first.

Testing

  • tsc --noEmit is clean, and the test suite passes 104/104 (51 before). The new init, skill-loader and adapter tests fail on main and pass here.
  • End-to-end checks:
    • init for every template.
    • validate.
    • export in all 15 formats for 13 agents (195 outputs). The output is byte-identical to main, and no agent dir is modified.
    • import from 6 tools.
    • install with good and bad entries.
    • run with stub claude and gemini binaries.

Follow-up

A second review found some smaller hardening items. They are not regressions and will come in a separate PR.

Add a shared path-safety helper that resolves a relative path inside a
base directory (rejecting absolute paths, '..' escapes and symlinks that
lead outside) and use it for always_load knowledge documents in every
adapter. Documents outside knowledge/ are skipped with a warning.

Also tolerate an empty knowledge/index.yaml instead of crashing.
…ent dir

Dependency mount paths must now be relative paths strictly inside the
agent directory; absolute paths, '..' escapes and the agent root itself
are rejected. Dependency names are confined to .gitagent/deps.

Local source/extends paths must resolve inside the agent directory
unless the new --allow-external-sources flag is passed. A local source
that already lives at its mount point is left in place instead of being
removed and re-copied. Pass '--' before the URL in git clone.
- Confine hooks[].script to the hooks/ directory and shell-quote the
  script path in the generated hook command so paths with spaces work.
- Pass system prompts larger than 100 KB via --append-system-prompt-file
  instead of argv to avoid E2BIG; smaller prompts are still inlined.
- Validate session options before writing any temp files and always
  remove temp files on every return path.
Default user_id is now LYZR_USER_ID if set, otherwise a stable
identifier derived from a SHA-256 hash of the key. --user-id still
overrides.
Use execFileSync with argument arrays for all git and gh calls (the PR
body is passed as a single argument), validate the agent name against
^[a-z][a-z0-9-]*$, parse and normalize the repository URL with URL(),
and remove the temp clone with rmSync. Fixes submissions from paths
containing spaces.
Invoke cmd.exe explicitly with every argument quoted and refuse
arguments containing characters cmd.exe still interprets inside quotes.
Set process.exitCode instead of calling process.exit so callers can
clean up.
Create config directories with 0700 and credential files with 0600,
merge the generated provider/profile into an existing OpenClaw or
Nanobot config instead of replacing it, and abort with an error rather
than overwriting a config file that cannot be parsed.
Previously only llm-wiki created --dir, so minimal/standard/full failed
for a new directory. Unknown templates are rejected before anything is
created.
- Create the target directory before importing.
- Copy Claude skill directories recursively (skipping symlinks) instead
  of reading every entry as a file.
- parseSections keeps text before the first heading and ignores
  headings inside fenced code blocks.
- Accept CrewAI's top-level agents.yaml map as well as an agents: key,
  and sanitize sub-agent directory names.
Parent (extends) skills are now merged in memory and referenced from
.gitagent/parent/skills instead of being copied into skills/.
Branch on whether the exported config has an agents map rather than on
whether sub-agent directories exist, always set agents.main.workspace,
and give every declared sub-agent a temp workspace.
Normalize line endings before matching frontmatter (also for Cursor
.mdc files) and accept allowed-tools as either a space-delimited string
or a YAML list.
- gitclaw runner copies hook scripts, knowledge documents and full skill
  directories into the temp workspace.
- GitHubProvider validates owner/repo references, skill sub-paths and
  install directory names.
- run/git runner: avoid process.exit inside try/finally, register temp
  clone cleanup on exit, and reuse the already-resolved clone for
  'run -a git' instead of cloning twice.
- opencode runner sets process.exitCode instead of exiting.
- Pass '--' before repository URLs and paths in git invocations.
npm update + npm audit fix (no --force); npm audit reports 0
vulnerabilities.

@shreyas-lyzr shreyas-lyzr left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the full finding list from #107 and the verification report. All six security findings and the functional bugs are fixed; test coverage is real and adversarial. Approving.

What I checked

Knowledge path traversal → LLM exfil (Critical): src/utils/path-safety.ts introduces resolveInside(), which resolves the path lexically, rejects absolute paths and '..' escapes, then follows symlinks (realpathSync on the deepest existing ancestor) and verifies the real target also stays inside the base. All 9 adapters now call loadKnowledgeDocument(knowledgeDir, doc.path) instead of join + readFileSync. The null guard on index?.documents also fixes the empty-index crash (finding #15). Verified in src/utils/path-safety.ts.

Install mount path traversal with rm -rf (Critical): src/commands/install.ts introduces resolveInstallTarget() which calls resolveInside(agentDir, dep.mount); an absolute path, '..', '.' or a path that resolves outside agentDir returns an error string and the install is aborted before any rmSync is called. Local source: / extends: paths outside the agent dir now require --allow-external-sources. Dependency names go through resolveInside(depsDir, dep.name) similarly.

Lyzr API key as user_id (High): resolveLyzrUserId() in src/runners/lyzr.ts: precedence is explicit userId arg, then LYZR_USER_ID env, then a sha256 hex digest of the API key (first 16 chars) prefixed 'gitagent-'. The raw API key is never sent as user_id. Verified in src/runners/lyzr.ts.

Registry shell injection (High): src/commands/registry.ts fully replaces all execSync template-string calls with execFileSync + argv arrays. Agent names are validated against /^[a-z][a-z0-9-]*$/ and repo URLs are parsed with new URL() and checked for https, github.com hostname, exactly two path segments, no auth/port/query/fragment. Verified in src/commands/registry.ts.

Gemini runner shell: true on Windows (High): shell: true is removed entirely. On non-Windows, spawnSync('gemini', args) is called directly. On Windows, cmd.exe is invoked explicitly via /d /s /c with windowsVerbatimArguments: true. Before reaching that path, every argument is checked by isSafeWindowsCmdArg() which rejects characters cmd.exe interprets inside quotes (", %, !, \n). Verified in src/runners/gemini.ts.

Credential files world-readable (High): src/utils/auth-provision.ts writes credential files with writeFileSync(path, ..., { mode: 0o600 }) and creates their parent directories with mkdirSync(dirname(path), { recursive: true, mode: 0o700 }). The temp prompt file in the Claude runner also uses mode 0o600. Verified at auth-provision.ts:41-42 and runners/claude.ts:138.

init -d ENOENT (correctness): init.ts:538 now calls mkdirSync(dir, { recursive: true }) before any createFile() call. Verified.

Claude runner E2BIG (correctness): System prompts > 100 KB are written to a temp file and passed via --append-system-prompt-file, bypassing ARG_MAX. Verified in src/runners/claude.ts.

Skill-loader CRLF and list-form allowed-tools (correctness): cursor.ts parseMdcFile now normalizes CRLF before the regex match. skill-loader.ts coerces allowed-tools to a string via Array.isArray(tools) ? tools.join(', ') : tools before calling .trim(). Both verified in the respective files.

CLAUDE.md / AGENTS.md / CONTRIBUTING.md: None present; no convention violations to check.

Security pass

Debug leftovers: No console.log, debugger hooks, .only tests, or debug flags added in the diff.

Secrets: No hardcoded credentials in the diff.

Dependencies (resolved versions from lockfile):

  • js-yaml 4.3.2: no known advisory — fixes CVE-2025-23059.
  • ajv 8.20.0 (lockfile resolved): no known advisory (GHSA-2g4f-4pwh-qvx6 affects 8.17.1, fixed before 8.20.0).
  • fast-uri 3.1.8: no known advisory.
  • npm audit: 0 vulnerabilities.

No injection, SSRF, authz, or crypto findings in the changed code.

Testing

src/utils/path-safety.test.ts tests isPathInside, resolveInside, and loadKnowledgeDocument with actual symlinks pointing outside the base, and then runs all 9 adapters against an agent with a traversal path and a symlink escape in knowledge/index.yaml, asserting the in-tree marker is included and the outside/symlink markers are not. The install tests cover mount traversal, external-source rejection, and valid installs. The registry tests cover name and URL validation. 104 tests claimed; test file structure is consistent with that count.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security & Bug Report: Path traversal, shell injection, API key leak, and more (15+ findings)

2 participants