Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
147 changes: 147 additions & 0 deletions gems/bsv-sdk/CVE-2026-40069.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
---
gem: bsv-sdk
cve: 2026-40069
ghsa: 9hfr-gw99-8rhx
url: https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-9hfr-gw99-8rhx
title: bsv-sdk ARC broadcaster treats INVALID/MALFORMED/ORPHAN
responses as successful broadcasts
date: 2026-04-09
description: |
# ARC broadcaster treats failure statuses as successful broadcasts

## Summary

`BSV::Network::ARC`'s failure detection only recognises `REJECTED`
and `DOUBLE_SPEND_ATTEMPTED`. ARC responses with `txStatus` values
of `INVALID`, `MALFORMED`, `MINED_IN_STALE_BLOCK`, or any
`ORPHAN`-containing `extraInfo` / `txStatus` are silently treated
as successful broadcasts. Applications that gate actions on broadcaster
success are tricked into trusting transactions that were never
accepted by the network.

## Details

`lib/bsv/network/arc.rb` (lines ~74-100 in the affected code) uses a
narrow failure predicate compared to the TypeScript reference SDK.
The TS broadcaster additionally recognises:

- `INVALID`
- `MALFORMED`
- `MINED_IN_STALE_BLOCK`
- Any response containing `ORPHAN` in `extraInfo` or `txStatus`

The Ruby implementation omits all of these, so ARC responses
carrying any of these statuses are returned to the caller as
successful broadcasts.

Additional divergences in the same module compound the risk:

- `Content-Type` is sent as `application/octet-stream`; the TS
reference sends `application/json` with a `{ rawTx: <hex> }`
body (EF form where source transactions are available).
- The headers `XDeployment-ID`, `X-CallbackUrl`, and `X-CallbackToken`
are not sent.

The immediate security-relevant defect is the missing failure
statuses; the other divergences are fixed in the same patch for
protocol compliance.

## Impact

Integrity: callers receive a success response for broadcasts that
were actually rejected by the ARC endpoint. Applications and
downstream gems that gate actions on broadcaster success — releasing
goods, marking invoices paid, treating a token as minted, progressing
a workflow — are tricked into trusting transactions that were never broadcast.

This is an integrity bug with security consequences. It does not
disclose information (confidentiality unaffected) and does not
affect availability.

## CVSS rationale

`AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N` → **7.5 (High)**

- **AV:N** — network-reachable.
- **AC:L** — no specialised access conditions are required. Triggering
any of the unhandled failure statuses is not meaningfully harder
than broadcasting a transaction at all: a malformed or invalid
transaction, an orphan condition from a transient fork, or a
hostile/misbehaving ARC endpoint returning one of these statuses
is sufficient. The attacker does not need to defeat any mitigation
or race a specific window — the bug is that the code path doesn't
exist at all.
- **PR:N** — no privileges required.
- **UI:N** — no user interaction.
- **C:N** — no confidentiality impact.
- **I:H** — downstream integrity decisions are taken on
non-broadcast transactions.
- **A:N** — no availability impact.

## Affected versions

The ARC broadcaster was introduced in commit `a1f2e62` ("feat(network):
add ARC broadcaster with injectable HTTP client") on 2026-02-08 and
first released in **v0.1.0**. The narrow failure predicate has been
present since introduction. Every release up to and including **v0.8.1**
is affected.

Affected range: `>= 0.1.0, < 0.8.2`.

## Patches

Upgrade to `bsv-sdk >= 0.8.2`. The fix:

- Expands the failure predicate (`REJECTED_STATUSES` + `ORPHAN`
substring check on both `txStatus` and `extraInfo`) to include
`INVALID`, `MALFORMED`, `MINED_IN_STALE_BLOCK`, and any
orphan-containing response, matching the TypeScript reference.
- Switches `Content-Type` to `application/json` with a `{ rawTx: <hex> }`
body, preferring Extended Format (BRC-30) hex when every input has
`source_satoshis` and `source_locking_script` populated and falling
back to plain raw-tx hex otherwise.
- Adds support for the `XDeployment-ID` (default: random
`bsv-ruby-sdk-<hex>`), `X-CallbackUrl`, and `X-CallbackToken`
headers via new constructor keyword arguments.

Fixed in sgbett/bsv-ruby-sdk#306.

### Note for `bsv-wallet` consumers

The sibling gem `bsv-wallet` (published from the same repository) is
not independently vulnerable — `lib/bsv/network/arc.rb` is not bundled
into the wallet gem's `files` list. However, `bsv-wallet` runtime-depends
on `bsv-sdk`, so a consumer of `bsv-wallet` that also invokes the
ARC broadcaster is transitively exposed whenever `Gemfile.lock`
resolves to a vulnerable `bsv-sdk` version. `bsv-wallet >= 0.3.4`
tightens its `bsv-sdk` constraint to `>= 0.8.2, < 1.0`, so upgrading
either gem is sufficient to pull in the fix.

## Workarounds

If upgrading is not immediately possible:

- Verify broadcast results out-of-band (e.g. query a block explorer
or WhatsOnChain) before treating a transaction as broadcast.
- Do not gate integrity-critical actions solely on the ARC
broadcaster's success response.

## Credit

Identified during the 2026-04-08 cross-SDK compliance review,
tracked as finding F5.13.
cvss_v3: 7.5
unaffected_versions:
- "< 0.1.0"
patched_versions:
- ">= 0.8.2"
related:
url:
- https://nvd.nist.gov/vuln/detail/CVE-2026-40069
- https://github.com/sgbett/bsv-ruby-sdk/releases/tag/v0.8.2
- https://github.com/sgbett/bsv-ruby-sdk/pull/306
- https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc
- https://github.com/sgbett/bsv-ruby-sdk/issues/305
- https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-9hfr-gw99-8rhx
- https://advisories.gitlab.com/pkg/gem/bsv-sdk/CVE-2026-40069
- https://github.com/advisories/GHSA-9hfr-gw99-8rhx
236 changes: 236 additions & 0 deletions gems/bsv-sdk/CVE-2026-40070.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
---
gem: bsv-sdk
cve: 2026-40070
ghsa: hc36-c89j-5f4j
url: https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-hc36-c89j-5f4j
title: bsv-sdk and bsv-wallet persist unverified certifier signatures
in acquire_certificate (direct and issuance paths)
date: 2026-04-09
description: |
# Unverified certifier signatures persisted by `acquire_certificate`

## Affected packages

Both `bsv-sdk` and `bsv-wallet` are published from the
[sgbett/bsv-ruby-sdk](https://github.com/sgbett/bsv-ruby-sdk)
repository. The vulnerable code lives in
`lib/bsv/wallet_interface/wallet_client.rb`, which is **physically
shipped inside both gems** (the `bsv-wallet.gemspec` `files` list
bundles the entire `lib/bsv/wallet_interface/` tree). Consumers of
either gem are independently vulnerable; the two packages are
versioned separately, so each has its own affected range.

| Package | Affected | Patched |
| --- | --- | --- |
| `bsv-sdk` | `>= 0.3.1, < 0.8.2` | `0.8.2` |
| `bsv-wallet` | `>= 0.1.2, < 0.3.4` | `0.3.4` |

## Summary

`BSV::Wallet::WalletClient#acquire_certificate` persists certificate
records to storage **without verifying the certifier's signature**
over the certificate contents. Both acquisition paths are affected:

- `acquisition_protocol: 'direct'` — the caller supplies all certificate
fields (including `signature:`) and the record is written to storage verbatim.
- `acquisition_protocol: 'issuance'` — the client POSTs to a certifier
URL and writes whatever signature the response body contains, also
without verification.

An attacker who can reach either API (or who controls a certifier
endpoint targeted by the issuance path) can forge identity certificates
that subsequently appear authentic to `list_certificates` and `prove_certificate`.

## Details

BRC-52 requires a certificate's `signature` field to be verified against
the claimed certifier's public key over a canonical hashing of
`(type, subject, serialNumber, revocationOutpoint, fields)` before
the certificate is trusted. The reference TypeScript SDK enforces
this in `Certificate.verify()`.

### Direct path

The Ruby implementation's `acquire_via_direct` path
(`lib/bsv/wallet_interface/wallet_client.rb`) constructs the certificate
record directly from caller-supplied fields:

```ruby
def acquire_via_direct(args)
{
type: args[:type],
subject: @key_deriver.identity_key,
serial_number: args[:serial_number],
certifier: args[:certifier],
revocation_outpoint: args[:revocation_outpoint],
signature: args[:signature],
fields: args[:fields],
keyring: args[:keyring_for_subject]
}
end
```

The returned record is then written to the storage adapter by
`acquire_certificate`. No verification of `args[:signature]` against
`args[:certifier]`'s public key occurs at any point in this path.

### Issuance path

`acquire_via_issuance` POSTs to a certifier-supplied URL and parses
the response body into a certificate record, which is then written
to storage without verifying the returned signature. A hostile or
compromised certifier endpoint — or anyone able to redirect/MITM the
plain HTTP request — can therefore return an arbitrary `signature`
value for any subject and have it stored as authentic. This is the
same class of bypass as the direct path; it was tracked separately
as finding **F8.16** in the compliance review and is closed by the same fix.

### Downstream impact

Downstream reads via `list_certificates` and selective-disclosure via
`prove_certificate` treat stored records as valid without re-verifying,
so any forgery that slips past `acquire_certificate` is trusted permanently.

## Impact

Any caller that can invoke `acquire_certificate` — via either
acquisition protocol — can forge a certificate attributed to an
arbitrary certifier identity key, containing arbitrary fields, and
have it persisted as authentic. Applications and downstream gems
that rely on the wallet's certificate store as a source of truth
for identity attributes (e.g. KYC assertions, role claims,
attestations) are subject to credential forgery.

This is a credential-forgery primitive, not merely a spec
divergence from BRC-52.

## CVSS rationale

`AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N` → **8.1 (High)**

- **AV:N** — network-reachable in any wallet context that
exposes `acquire_certificate` to callers.
- **AC:L** — low attack complexity: pass arbitrary bytes as `signature:`.
- **PR:L** — low privileges: any caller authorised to invoke
`acquire_certificate`.
- **UI:N** — no user interaction required.
- **C:H** — forged credentials via `prove_certificate` can assert
attributes about the subject.
- **I:H** — the wallet's credential store is polluted with
attacker-controlled data.
- **A:N** — availability unaffected.

## Proof of concept

```ruby
client = BSV::Wallet::WalletClient.new(key,
storage: BSV::Wallet::MemoryStore.new)

client.acquire_certificate(
type: 'age-over-18',
acquisition_protocol: 'direct',
certifier: claimed_trusted_pubkey_hex,
serial_number: 'any-serial',
revocation_outpoint: ('00' * 32) + '.0',
signature: 'deadbeef' * 16, # arbitrary bytes — never verified
fields: { 'verified' => 'true' },
keyring_for_subject: {}
)

client.list_certificates(
certifiers: [claimed_trusted_pubkey_hex],
types: ['age-over-18']
)
# => returns the forged record as if it were a real
# certificate from that certifier
```

## Affected versions

The vulnerable direct-path code was introduced in commit `d14dd19`
("feat(wallet): implement BRC-100 identity certificate methods
(Phase 5)") on 2026-03-27 20:35 UTC. The vulnerable issuance-path
code was added one day later in `6a4d898` ("feat(wallet): implement
certificate issuance protocol", 2026-03-28 04:38 UTC), which removed
an earlier `raise UnsupportedActionError` and replaced it with an
unverified HTTP POST.

**`bsv-sdk`:** the v0.3.1 chore bump (`89de3a2`) was committed
28 minutes after `d14dd19`, so the direct-path bypass shipped in
the **v0.3.1** tag. The v0.3.1 release raised `UnsupportedActionError`
for the issuance path, so the issuance-path bypass first shipped in
**v0.3.2** (`5a335de`). Every subsequent release up to and including
**v0.8.1** is affected by at least one path, and every release
from v0.3.2 onwards is affected by both.
Combined affected range: `>= 0.3.1, < 0.8.2`.

**`bsv-wallet`:** at the time both commits landed, the wallet gem
was at version 0.1.1. The first wallet release containing any of
the vulnerable code was **v0.1.2** (`5a335de`, 2026-03-30), which
shipped both paths simultaneously. Every subsequent release up to
and including **v0.3.3** is affected on both paths.
Affected range: `>= 0.1.2, < 0.3.4`.

## Patches

Upgrade to `bsv-sdk >= 0.8.2` **and/or** `bsv-wallet >= 0.3.4`.
Both releases ship the same fix: a new module
`BSV::Wallet::CertificateSignature`
(`lib/bsv/wallet_interface/certificate_signature.rb`), which builds
the BRC-52 canonical preimage (`type`, `serial_number`, `subject`,
`certifier`, `revocation_outpoint`, lexicographically-sorted `fields`)
and verifies the certifier's signature against it via
`ProtoWallet#verify_signature` with protocol ID `[2, 'certificate signature']`
and counterparty = the claimed certifier's public key. Both
`acquire_via_direct` and `acquire_via_issuance` now call
`CertificateSignature.verify!` before returning the certificate to
`acquire_certificate`, so invalid certificates raise
`BSV::Wallet::CertificateSignature::InvalidError` (a subclass of
`InvalidSignatureError`) and are never written to storage.

Consumers should upgrade whichever gem they depend on directly; they
do not need both. `bsv-wallet 0.3.4` additionally tightens its
dependency on `bsv-sdk` from the stale `~> 0.4` to `>= 0.8.2, < 1.0`,
which forces the known-good pairing and pulls in the sibling
advisory fixes (F1.3, F5.13) tracked separately.

The issuance-path fix also partially closes finding **F8.16**
from the same compliance review. F8.16's second aspect — switching
the issuance transport from ad-hoc JSON POST to BRC-104 AuthFetch —
is not addressed here and remains deferred to a future release.

Fixed in sgbett/bsv-ruby-sdk#306.

## Workarounds

If upgrading is not immediately possible:

- Do not expose `acquire_certificate` (either acquisition
protocol) to untrusted callers.
- Do not invoke `acquire_certificate` with `acquisition_protocol:
'issuance'` against a certifier URL you do not fully trust, and
require TLS for any such request.
- Treat any record returned by `list_certificates` / `prove_certificate`
as unverified and perform an out-of-band BRC-52 verification against
the certifier's public key before acting on it.

## Credit

Identified during the 2026-04-08 cross-SDK compliance review, tracked
as findings F8.15 (direct path) and F8.16 (issuance path, partial).

cvss_v3: 8.1
unaffected_versions:
- "< 0.3.1"
patched_versions:
- ">= 0.8.2"
related:
url:
- https://nvd.nist.gov/vuln/detail/CVE-2026-40070
- https://github.com/sgbett/bsv-ruby-sdk/security/advisories/GHSA-hc36-c89j-5f4j
- https://github.com/sgbett/bsv-ruby-sdk/pull/306
- https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc
- https://github.com/sgbett/bsv-ruby-sdk/issues/305
- https://bsv.brc.dev/peer-to-peer/0052
- https://advisories.gitlab.com/pkg/gem/bsv-sdk/CVE-2026-40070/
- https://github.com/advisories/GHSA-hc36-c89j-5f4j
Loading
Loading