Files
composiohq__composio/ts
Alberto Schiabel f926058147 fix(cli): refuse symlink entries when extracting archives (#4210)
Addresses the `pnpm audit --prod` warning that has been riding along on
recent CLI PRs: **`extract-zip` unvalidated symlink path traversal**,
[GHSA-jmr9-qjv8-65gv](https://github.com/advisories/GHSA-jmr9-qjv8-65gv)
/ CVE-2026-56876, high.

## There is no version to upgrade to

`pnpm audit` renders "Patched versions >=2.0.2", which reads like a bump
would fix it. It would not:

- `npm view extract-zip versions` ends at **2.0.1** — no 2.0.2 was ever
published
- the advisory itself records `"first_patched_version": null`

So `pnpm audit --fix --prod` cannot resolve this, and neither can a
version constraint.

## What the flaw actually is

I reproduced it rather than working from the summary. `extract-zip`
2.0.1 **does** block writes that traverse a symlink — an archive with
`link -> ../../ESCAPED` plus `link/pwned.txt` is refused with `Out of
bound path ... while processing file link/pwned.txt`, and nothing lands
outside the target.

What it does not do is validate the symlink itself. An archive whose
only entry is `escape-abs -> /tmp/ABSOLUTE-TARGET` extracts cleanly and
plants that dangling symlink in the output directory. Per the advisory:
*"Depending on how extract-zip is used, an attacker could read or write
to arbitrary files."* Every call site here extracts an archive and then
reads or copies files back out of the tree, which is exactly the shape
that turns a planted symlink into an arbitrary-path read.

## Why not swap the library

`#4183` moved tar extraction to `Bun.Archive`, so that was the obvious
candidate. It does not work: **`Bun.Archive` cannot read zip archives at
all.** A benign zip fails with `ReadError`, same as the malicious ones —
those "refusals" were read failures, not security. Verified before
ruling it out.

## Why not fix it upstream

Because it already is, three times over, and none of it has moved:

| PR | Date | Targets |
| --- | --- | --- |
|
[max-mapper/extract-zip#158](https://github.com/max-mapper/extract-zip/pull/158)
| 2026-08-14 | GHSA-jmr9-qjv8-65gv, this advisory |
| [#160](https://github.com/max-mapper/extract-zip/pull/160) |
2026-08-17 | CVE-2026-19693, symlink at the final path component |
| [#161](https://github.com/max-mapper/extract-zip/pull/161) |
2026-08-20 | CVE-2026-56876 again, and bumps the version to 2.0.2 |

All three are mergeable with zero review comments. The repository's last
commit to `master` was **August 2021**; 2.0.1 shipped June 2020. [Issue
#159](https://github.com/max-mapper/extract-zip/issues/159), a
downstream consumer asking whether a patched release is planned, has
three replies — every one from another stranded consumer, none from a
maintainer.

A fourth PR would not be a contribution. And our implementation is the
wrong shape to upstream regardless: refusing *every* symlink is right
for an application whose archives never carry one, but a breaking change
for a general-purpose library, where the correct fix is validating that
a link's resolved target stays inside the extraction root — which is
what #158 and #161 already do. Upstream even tracks "do not extract
symlinks" as a separate [feature
request](https://github.com/max-mapper/extract-zip/issues/140), open
since 2023.

So this is a deliberate local workaround on a timeline we control. The
helper's doc comment records the tracking PRs and the condition for
deleting it: a published release that actually carries the fix.

## The fix

No archive either package extracts — release binaries, companion assets,
skills, sidecar bundles — legitimately contains a symlink. So the entry
type is refused outright, via `extract-zip`'s own `onEntry` hook, which
runs **before** the entry is written. A rejected archive plants nothing.

All four call sites now go through it:

| package | call site |
| --- | --- |
| `cli` | `services/upgrade-binary.ts` |
| `cli` | `services/run-companion-modules.ts` |
| `cli` | `effects/install-skill.ts` |
| `cli-local-tools` | `src/bundled-binaries.ts` |

Each package carries its own copy of the helper rather than sharing one.
`run-companion-modules.ts` is bundled as a standalone companion module
and imports nothing from `@composio/cli-local-tools`; routing it through
that package to share ~15 lines would pull the local-tools tree into
those bundles.

## Verification

Committed three fixture archives (827 bytes total) and tested against
real zips, not mocks:

- benign archive extracts, contents intact
- `symlink-absolute.zip` (`-> /tmp/ABSOLUTE-TARGET`) refused, output
directory left empty
- `symlink-relative.zip` (`-> ../../outside`) refused, output directory
left empty
- `isSymlinkZipEntry` unit-tested across symlink, regular file,
directory, and Windows-authored entries with no Unix mode

**Mutation-checked:** neutering the predicate makes exactly the three
symlink tests fail and leaves the other four green, so the suite
genuinely detects the regression rather than passing by construction.

Suites: `@composio/cli` 123 files / 1263 passed,
`@composio/cli-local-tools` 7 files / 20 passed. Typecheck clean on
both. `pnpm validate:changesets` passes; note lands in
`ts/packages/cli/CHANGELOG.md`.

`extract-zip` stays as the dependency — it is still the only working zip
reader here, and with symlink entries refused the advisory's vector no
longer applies to this codebase.

https://claude.ai/code/session_01JkwtxPHfobZxzvAqe53x62
2026-08-23 12:28:24 +02:00
..

Composio TypeScript workspace

This directory contains the TypeScript half of the Composio SDK monorepo: the core SDK, provider adapters, the CLI, examples, and end-to-end tests. For an overview of Composio itself, start at the root README and docs.composio.dev.

If you just want to use the SDK:

npm install @composio/core
import { Composio } from '@composio/core';

const composio = new Composio({ apiKey: process.env.COMPOSIO_API_KEY });

const session = await composio.create('user_123');
const tools = await session.tools();

See the @composio/core README and the quickstart for the full flow, including provider setup for your agent framework.

Packages

Published packages:

Package Description
@composio/core The Composio SDK. Ships its TypeScript source and SDK docs so installed copies are inspectable by coding agents.
@composio/slim Same API as @composio/core without the packaged source and docs; smaller install.
composio CLI Standalone CLI binary: search, execute, and script tools from your shell.
@composio/* providers Adapters that format Composio tools for agent frameworks (OpenAI, Anthropic, Vercel AI SDK, LangChain, and more). See the provider table.
@composio/experimental Experimental integrations, currently the Pi provider.
@composio/json-schema-to-zod JSON Schema to Zod conversion.

Internal (unpublished) packages: cli-keyring and cli-local-tools support the CLI; ts-builders generates TypeScript source.

Layout

ts/
  packages/        Published and internal packages (see above)
  examples/        Runnable examples per feature and framework
  e2e-tests/       Runtime E2E tests (Node, Deno, Cloudflare Workers, CLI)
  docs/            Workspace SDK docs: API notes and internal guides
  scripts/         Build, validation, and scaffolding scripts
  vendor/          Read-only reference submodules; do not edit

Development

Commands run from the repository root. Install the pinned toolchain first:

mise install
pnpm install

Build and verify:

pnpm build:packages   # build all TS packages
pnpm typecheck        # typecheck all TS packages
pnpm lint:packages    # oxlint over ts/packages
pnpm test             # package unit tests plus example validation

Runtime E2E suites (require credentials):

pnpm test:e2e:node
pnpm test:e2e:deno
pnpm test:e2e:cloudflare
pnpm test:e2e:cli

Scaffolding:

pnpm create:provider <name> [--agentic]   # new provider package
pnpm create:example <name>                # new example under ts/examples

Changesets are required for changes to published packages; see the contribution guidelines.

Support