From ab602cfd16e250718845aab3ed822d914ed2aa0f Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Wed, 15 Jul 2026 10:34:13 +0200 Subject: [PATCH 1/2] Fix security findings from 6.1.0 audit --- features/support/hooks.rb | 2 +- lib/pubnub/cbor.rb | 70 ++++++++++++++++++++++++++----------- lib/pubnub/constants.rb | 2 +- lib/pubnub/event.rb | 2 +- spec/helpers/spec_helper.rb | 3 ++ 5 files changed, 56 insertions(+), 23 deletions(-) diff --git a/features/support/hooks.rb b/features/support/hooks.rb index 876d9a379..df48f004d 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -12,7 +12,7 @@ expect(ENV['SERVER_PORT']).not_to be_nil @pn_configuration = { origin: ENV['SERVER_HOST'] + ":" + ENV['SERVER_PORT'], - isSecure: false, + ssl: false, } } diff --git a/lib/pubnub/cbor.rb b/lib/pubnub/cbor.rb index 4b992dd93..2a50363d5 100644 --- a/lib/pubnub/cbor.rb +++ b/lib/pubnub/cbor.rb @@ -1,5 +1,10 @@ module Pubnub class Cbor + class DecodeError < StandardError; end + + MAX_DEPTH = 32 + MAX_CONTAINER_LENGTH = 65_536 + MAX_INPUT_SIZE = 1_048_576 private @@ -43,9 +48,14 @@ def bytearray_to_i(byte_array) end end + def take_bytes(data, count) + raise DecodeError, "Truncated CBOR input" if data.size < count + data.shift(count) + end + def decode_integer(data, additional) if ADDITIONAL_LENGTH_BYTES.member?(additional) - bytearray_to_i(data.shift(ADDITIONAL_LENGTH_BYTES[additional])) + bytearray_to_i(take_bytes(data, ADDITIONAL_LENGTH_BYTES[additional])) else additional end @@ -55,7 +65,10 @@ def decode_float(data, additional) if additional <= 23 additional else - bytes = bytearray_to_i(data.shift(ADDITIONAL_LENGTH_BYTES[additional])) + raise DecodeError, "Invalid float additional value" unless ADDITIONAL_LENGTH_BYTES.member?(additional) + + bytes = bytearray_to_i(take_bytes(data, ADDITIONAL_LENGTH_BYTES[additional])) + case (additional) when ADDITIONAL_LENGTH_1B bytes @@ -98,44 +111,57 @@ def indefinite_data(data) result = [] loop do + raise DecodeError, "Truncated CBOR input" if data.empty? byte = data.shift break if byte == INDEFINITE_BREAK result.append(byte) - break if data.empty? end result end - def compute_length(data, additional) - if ADDITIONAL_LENGTH_BYTES.member?(additional) - bytearray_to_i(data.shift(ADDITIONAL_LENGTH_BYTES[additional])) - else - additional + def compute_container_length(data, additional, min_element_bytes) + length = if ADDITIONAL_LENGTH_BYTES.member?(additional) + bytearray_to_i(take_bytes(data, ADDITIONAL_LENGTH_BYTES[additional])) + else + additional + end + + if length > MAX_CONTAINER_LENGTH + raise DecodeError, "CBOR container length #{length} exceeds maximum #{MAX_CONTAINER_LENGTH}" + end + + if length * min_element_bytes > data.size + raise DecodeError, "CBOR container length #{length} exceeds remaining input" end + + length end def decode_string(data, additional) if additional == ADDITIONAL_TYPE_INDEFINITE indefinite_data(data).pack('C*').force_encoding('UTF-8') else - length = compute_length(data, additional) - data.shift(length).pack('C*').force_encoding('UTF-8') + length = compute_container_length(data, additional, 1) + take_bytes(data, length).pack('C*').force_encoding('UTF-8') end end - def decode_map(data, additional) - length = compute_length(data, additional) + def decode_map(data, additional, depth) + length = compute_container_length(data, additional, 2) result = Hash.new - (1..length).each { result.store(parse_data(data), parse_data(data)) } + length.times { result.store(parse_data(data, depth), parse_data(data, depth)) } result end - def decode_array(data, additional) - length = compute_length(data, additional) - (1..length).map { parse_data(data) } + def decode_array(data, additional, depth) + length = compute_container_length(data, additional, 1) + Array.new(length) { parse_data(data, depth) } end - def parse_data(data) + def parse_data(data, depth = 0) + raise DecodeError, "CBOR nesting depth exceeds #{MAX_DEPTH}" if depth > MAX_DEPTH + raise DecodeError, "Truncated CBOR input" if data.empty? + byte = data.shift case (byte) @@ -163,9 +189,9 @@ def parse_data(data) when TYPE_TEXT_STRING decode_string(data, additional) when TYPE_ARRAY - decode_array(data, additional) + decode_array(data, additional, depth + 1) when TYPE_HASHMAP - decode_map(data, additional) + decode_map(data, additional, depth + 1) else nil end @@ -175,7 +201,11 @@ def parse_data(data) public def decode(value) - parse_data(value) + if value.size > MAX_INPUT_SIZE + raise DecodeError, "CBOR input size #{value.size} exceeds maximum #{MAX_INPUT_SIZE}" + end + + parse_data(value.dup) end end diff --git a/lib/pubnub/constants.rb b/lib/pubnub/constants.rb index dace30ac4..d7635fda2 100644 --- a/lib/pubnub/constants.rb +++ b/lib/pubnub/constants.rb @@ -20,7 +20,7 @@ module Constants DEFAULT_TTL = 1440 DEFAULT_REGION = '0'.freeze DEFAULT_USE_RANDOM_IV = true - DEFAULT_SSL = false + DEFAULT_SSL = true REQUEST_MESSAGE_COUNT_THRESHOLD = 0 MAXIMUM_HERE_NOW_COUNT = 1000 diff --git a/lib/pubnub/event.rb b/lib/pubnub/event.rb index 6c74cbc8f..ac4b84bfe 100644 --- a/lib/pubnub/event.rb +++ b/lib/pubnub/event.rb @@ -88,7 +88,7 @@ def uri(memo = true) uri += path uri += '?' + Formatter.params_hash_to_url_params(parameters) uri += "&signature=#{sa_signature}" if sa_signature - Pubnub.logger.debug('Pubnub::Event') { "Requested URI: #{uri}" } + Pubnub.logger.debug('Pubnub::Event') { "Requested URI: #{uri.gsub(/([?&](?:signature|pnsig)=)[^&#]*/i, '\1***')}" } URI uri end diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index be4b6ff8a..bdac90254 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -22,7 +22,10 @@ end require "pubnub" + Pubnub::Constants::DEFAULT_RECONNECT_INTERVAL = 0 +Pubnub::Constants::DEFAULT_SSL = false + if ENV["CI"] == "true" && ENV['NO_COVERAGE'] != 'true' require "codacy-coverage" Codacy::Reporter.start From ce4adb9ccfe9d24d7c934493f830b56e86245173 Mon Sep 17 00:00:00 2001 From: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> Date: Tue, 21 Jul 2026 08:46:04 +0000 Subject: [PATCH 2/2] PubNub SDK v6.1.1 release. --- .pubnub.yml | 17 +++++++++++++---- CHANGELOG.md | 8 ++++++++ Gemfile.lock | 2 +- VERSION | 2 +- lib/pubnub/version.rb | 2 +- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.pubnub.yml b/.pubnub.yml index 64e293437..6dadee3f1 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,15 @@ --- -version: "6.1.0" +version: "6.1.1" changelog: + - date: 2026-07-21 + version: v6.1.1 + changes: + - type: bug + text: "`Pubnub::Cbor` now enforces a recursion-depth cap, a per-container length cap, and a total input size cap. Malformed or attacker-crafted tokens reaching `Pubnub::Client#parse_token` raise `Pubnub::Cbor::DecodeError` instead of hanging the caller or triggering `SystemStackError`." + - type: bug + text: "`Pubnub::Constants::DEFAULT_SSL` flipped from `false` to `true`. Integrators must now opt out explicitly to send traffic in cleartext." + - type: bug + text: "The Requested URI debug entry in `Pubnub::Event#uri` now masks the `signature` and `pnsig` query parameter values." - date: 2026-06-15 version: v6.1.0 changes: @@ -647,7 +656,7 @@ sdks: - x86-64 - distribution-type: package distribution-repository: RubyGems - package-name: pubnub-6.1.0.gem + package-name: pubnub-6.1.1.gem location: https://rubygems.org/gems/pubnub requires: - name: addressable @@ -752,8 +761,8 @@ sdks: - x86-64 - distribution-type: library distribution-repository: GitHub release - package-name: pubnub-6.1.0.gem - location: https://github.com/pubnub/ruby/releases/download/v6.1.0/pubnub-6.1.0.gem + package-name: pubnub-6.1.1.gem + location: https://github.com/pubnub/ruby/releases/download/v6.1.1/pubnub-6.1.1.gem requires: - name: addressable min-version: 2.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index d00fff5bf..dba99b0ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v6.1.1 +July 21 2026 + +#### Fixed +- `Pubnub::Cbor` now enforces a recursion-depth cap, a per-container length cap, and a total input size cap. Malformed or attacker-crafted tokens reaching `Pubnub::Client#parse_token` raise `Pubnub::Cbor::DecodeError` instead of hanging the caller or triggering `SystemStackError`. +- `Pubnub::Constants::DEFAULT_SSL` flipped from `false` to `true`. Integrators must now opt out explicitly to send traffic in cleartext. +- The Requested URI debug entry in `Pubnub::Event#uri` now masks the `signature` and `pnsig` query parameter values, per the SDK log practices ADR. `auth` and `token` remain visible at DEBUG since they are permitted URL parameters. + ## v6.1.0 June 15 2026 diff --git a/Gemfile.lock b/Gemfile.lock index 0e7aa9285..385485b10 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - pubnub (6.1.0) + pubnub (6.1.1) addressable (>= 2.0.0) base64 concurrent-ruby (~> 1.3.4) diff --git a/VERSION b/VERSION index dfda3e0b4..f3b5af39e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -6.1.0 +6.1.1 diff --git a/lib/pubnub/version.rb b/lib/pubnub/version.rb index 43e928362..f4f9faec3 100644 --- a/lib/pubnub/version.rb +++ b/lib/pubnub/version.rb @@ -1,4 +1,4 @@ # Toplevel Pubnub module. module Pubnub - VERSION = '6.1.0'.freeze + VERSION = '6.1.1'.freeze end