Skip to content

Security: btclib-org/bitcoin-core-rpc

SECURITY.md

Security policy

Reporting a vulnerability

If you have found a security vulnerability, please do not open a GitHub issue: an issue is public from the moment it is filed, and so is the window between filing it and a fix being released.

Report it privately instead, by opening a security advisory. Only the maintainers can see it, the discussion stays private until an advisory is published, and a CVE can be requested from it if the vulnerability warrants one.

If you have no GitHub account, or would rather not use it for this, responsible disclosure by email to security at btclib dot org is equally welcome.

A report is acknowledged within 7 days, and a fix or a published advisory follows within 90 days.

A report triaged as easy to rediscover is fixed as soon as possible, since anyone can find it in the meantime, and its advisory is published with the fix.

What belongs here, and what belongs upstream

This project is one HTTP client. What belongs here is what it does with a credential and with a reply:

  • the credential: how it is built, where it is read from, and where it can end up. A credential in a url, in a __repr__, in an exception message or in a traceback is a defect here
  • where a request is sent: the scheme, the redirect that is refused, the proxy that is not taken from the environment
  • what is read back: the bound on the body, what is parsed and what is refused, and whether a reply can be attributed to the request that asked for it
  • the distributions published to PyPI and their provenance

What an exception, a repr or a log line may say about a credential is the organization's standard's A refusal carries no secret.

What a method does is Bitcoin Core's, and belongs to bitcoin/bitcoin. So does the node's own authentication.

Report it wherever you found it, though: routing a report is the maintainers' job, not the reporter's, and a doubt about which project owns a flaw is not a reason to keep it to yourself.

Security review

The latest security review is dated 2026-09-30. pmazzocchi did it against the assurance case, and issue 585 records it.

The review's scope is the tree at commit 002e57e0a6d6. What changed since is that commit's comparison with main.

Supported versions

Only the latest release is supported. A fix is published as a new release, and nothing is backported: an ordinary dependency bump is what carries it to an installed package.

Wheels and sdist are published to PyPI with PEP 740 attestations, through a workflow that no long-lived token can authenticate for (PyPI Trusted Publishing), so a distribution can be traced back to the workflow run and the commit it was built from.

The same files are attached to the GitHub release, and those copies carry a build provenance attestation of their own, signed in the run that built them:

signer=btclib-org/.github/.github/workflows/reusable-build.yml
id=$(gh api repos/btclib-org/bitcoin-core-rpc --jq .id)
cert='.[].verificationResult.signature.certificate'
gh attestation verify <file> --owner btclib-org \
  --signer-workflow "${signer:?}" \
  --source-ref refs/tags/v<version> --format json \
  --jq "${cert:?}.sourceRepositoryIdentifier" | grep -qx "${id:?}"

The exit code is 0 only where a verified attestation names this repository. The repository is compared by its id, which a rename keeps: --repo compares the current name and refuses every release signed before a rename.

<file> is a distribution file from the release.

--signer-workflow names the workflow that signed, and narrows what passes to it. For a release built by the organization's reusable-build.yml, which release.yml calls, that is that workflow: an attestation made inside a called workflow names the callee as its signer. Without the flag, any attestation this repository has passes, including one from a workflow it calls in btclib-org/.github. --source-ref is what keeps a build of a branch from passing as the release. A release from v2026.9.24 on, made before this repository called reusable-build.yml, was signed by reusable-attest.yml, named the same way, with the same --source-ref. From v2026.8.12 through v2026.9.3 the signer is release.yml itself, so for those releases signer is btclib-org/bitcoin-core-rpc/.github/workflows/release.yml. An earlier release carries no build provenance attestation. No path verifies a release another signed. The PEP 740 attestations on PyPI name release.yml, the job that uploads there being its own rather than a called workflow's. The signed statement for the GitHub release is attached to it as well, as <tag>.intoto.jsonl, or as <tag>.attestation.jsonl on a release that carries that name instead, so --bundle <that file> runs the same check reading it from disk instead of asking GitHub for it. One attestation covers the wheel, the sdist and the bill of materials; the bundle is that attestation and is not among its subjects.

A CycloneDX 1.6 bill of materials is attached beside them, bitcoin_core_rpc-<version>.cdx.json: the two files with their SHA-256, the licence, and one component per dependency the wheel's metadata declares. It is generated from the built wheel rather than from the source tree, so it describes the files it is attached to, and it is covered by the same attestation — a bill of materials whose provenance nobody can check says only what whoever wrote it wanted said. This package declares no dependency, so components is an empty list, and the signed emptiness is what the document is for here: without one a consumer has to go and establish it somewhere else.

Limitations, not vulnerabilities

These are known and inherent.

  • Basic authentication is cleartext over plain HTTP, that being what Core's rpc speaks. Core dropped -rpcssl to remove OpenSSL from the daemon. Its rpc does not use encryption and has not been hardened to withstand arbitrary Internet traffic, so it is not to be exposed to the Internet. This client adds no TLS of its own: the url's scheme decides. On loopback, which is what from_chain builds, the cleartext stays between one process and the node beside it. An https url gets the certificate and the host name verified, unless a context the caller gives says otherwise (below). For a node anywhere else over http, the cleartext is on the wire, and rpc credentials authorise every wallet command that node has; an https url or a tunnel (SSH, stunnel or a VPN) keeps them off it.
  • A credential handed to this client lives in Python objects, which are immutable and not zeroized: it stays in process memory until garbage collection, and may have been copied by the interpreter meanwhile. cookie_path is the mitigation that exists — the file is read at each call rather than held, so the credential lives for the duration of one call and the node rotates it at every restart.
  • SSL_CERT_FILE and SSL_CERT_DIR choose the trust store of an https connection given no context. The default TLS context verifies the certificate and the host name against the system's default certificates, which OpenSSL reads from those two variables when they are set, so whoever controls this process's environment chooses which authorities it trusts.
  • A context the caller gives decides what is verified. urlopen_transport and SessionTransport take an ssl.SSLContext as context, and use it as given: one that trusts a node's own authority reaches that node without the environment, and one with verification off verifies nothing. It keeps its key log too: one built by ssl.create_default_context() while SSLKEYLOGFILE was set writes the session keys to that file.
  • A caller's own transport is outside all of this. It does its own I/O, so what it holds in memory, whether it follows a redirect and where it sends the Authorization header are its author's to answer for. The two arguments it is handed are the whole of what this module can say about it.
  • getblockchaininfo is not asked on the caller's behalf. Nothing here checks which chain a node serves, so a client pointed at a testnet node answers testnet questions without saying so. Ask it, and read the answer through network_from_chain.
  • There is no retry, and that is deliberate, but it means a 503 from a full work queue is a failure the caller has to decide about. The reason the decision is theirs is that call carries any method and a timeout is not a deadline: a node that stopped answering may still be executing the call, so a client re-sending a wallet command of its own accord can execute it twice.
Learn more about advisories related to btclib-org/bitcoin-core-rpc in the GitHub Advisory Database