CodeVetter · Repo Unpack · Risk map
httpie/cli
The sharpest edges are in code that leaves the process or touches the filesystem outside the repo: the update daemon double-forks and re-executes the CLI, the plugin installer shells out to pip and then unlinks files by hand, and man-page display execs `man`. TLS handling has two deliberate-but-loud decisions -- a global `urllib3.disable_warnings()` and a `verify=False` fetch of the version index. Filesystem risk concentrates in download filename derivation and lock-file handling in the system temp dir. Structurally, `core.program()` and `_process_auth()` are both flagged in-repo as needing refactoring, and the argparse Namespace is a mutable god-object threaded through most modules, so changes to parse ordering have wide blast radius.
- `_fetch_updates` performs `requests.get(PACKAGE_INDEX_LINK, verify=False)` against https://packages.httpie.io/latest.json -- an unauthenticated, certificate-unverified network call whose result drives the version-comparison warning shown to users.
- `client.py` calls `urllib3.disable_warnings()` unconditionally at import, which silences InsecureRequestWarning process-wide -- including for `--verify=no` -- so a user disabling verification gets no library-level warning.
- Daemon mode is decided by a raw substring check (`'--daemon' in args`) before argparse ever runs, so the flag is reachable from ordinary argv; the blast radius is limited only by the `assert options.task_id in DAEMONIZED_TASKS` in `run_daemon_task`, and asserts vanish under `python -O`.
- `_spawn_posix` double-forks, calls `os.setsid()`, closes stdin/stdout/stderr, and then re-enters `httpie.core.main(['http'] + args)` in-process with `suppress(BaseException)` -- a path that is effectively untestable in-process and whose failures are silent by construction.
- `spawn_daemon` copies the full parent environment into the child process context and injects a computed `PYTHONPATH` derived from `inspect.stack()[0][1]`, so anything in the caller's env (including secrets) is inherited by the detached process.
- `PluginInstaller._uninstall` deletes files directly with `os.unlink(distribution.locate_file(file))` for every entry in `distribution.files`; the only guard is a check that `self.dir` is in `base_dir.parents`, and the code's own TODO notes failures are not rolled back.
- `_clear_metadata` calls `shutil.rmtree(meta_path)` on `.dist-info`/`.egg-info` directories whose version differs from the freshly installed one, driven by parsing the last line of pip's stdout (`Successfully installed ...`) -- a string-parsing dependency on pip's output format.
- Plugin loading executes arbitrary third-party code at startup: `load_installed_plugins` calls `entry_point.load()` for every matching entry point, catching only `BaseException` into a warning -- so a malicious or broken plugin on the plugins path runs on every `http` invocation.
- Download filenames are attacker-influenced: `filename_from_content_disposition` takes the server-supplied header and sanitizes it only with `os.path.basename(...).lstrip('.').strip()`, after which `get_unique_filename` opens the result for append in the current working directory.
- `open_with_lockfile` derives a lock path as base64 of the target path inside the shared system temp dir and relies on `touch(exist_ok=False)` for mutual exclusion; the lock is only removed in the finally block, so a hard kill leaves a stale lock that permanently raises `LockFileError` for that file.
- Credentials flow into session files: when `--auth` is given alongside `--session`, `collect_messages` writes `{'type': auth_type, 'raw_auth': plugin.raw_auth}` into the session JSON, meaning plaintext `user:password` is persisted to disk under the config directory.
- `_process_auth` also silently harvests credentials from two implicit sources -- a `user:pass@host` URL and the user's netrc via `get_netrc_auth` (for plugins with `netrc_parse = True`, i.e. basic and digest) -- unless `--ignore-netrc` forces an `ExplicitNullAuth`.
- Two subprocess shell-outs exist outside the plugin installer: `man_pages.is_available`/`display_for` run `man 1 <program_name>` with `env.program_name` derived from argv[0], and the linux packaging build invokes `pyinstaller` via `subprocess.check_call`.
- Two central functions carry explicit in-repo refactor warnings: `core.program()` is marked 'TODO: Refactor and drastically simplify, especially so that the separator logic is elsewhere' and `_process_auth` is marked 'TODO: refactor & simplify this method' -- both are on every request path.
- `_setup_standard_streams` mutates `env.stdout`/`env.stdout_isatty` in place during parsing (redirecting stdout to stderr for `--download`, to devnull for `--quiet`), so any later code that captured the original stream sees stale state -- a fragile coupling the code itself labels 'FIXME: Come up with a cleaner solution'.
- The legacy session-format modules are live compatibility shims rather than dead code -- `pre_process`/`post_process` run on every session load and save, and the 3.1.0 fixer exists because domainless cookies were a documented security problem.
- `HTTPieCookiePolicy` widens cookie security by treating `localhost` and `*.localhost` as secure origins, so `Secure` cookies are sent over plain http to those hosts -- intentional (mirrors Firefox) but a deviation from the stdlib default that affects every request.