From fd9d9457f0fd1ced3f699aacc811551f7d2ed46d Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Wed, 24 Jun 2026 14:01:03 +0200 Subject: [PATCH 1/3] Fix N1QL injection in nested Hash attribute updates In build_update, Hash values were interpolated directly into the query string without sanitization, allowing N1QL injection. Use quote() to properly escape values. Co-Authored-By: Claude Opus 4.6 --- lib/couchbase-orm/relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchbase-orm/relation.rb b/lib/couchbase-orm/relation.rb index fc49511..99d8026 100644 --- a/lib/couchbase-orm/relation.rb +++ b/lib/couchbase-orm/relation.rb @@ -217,7 +217,7 @@ def build_update(**cond) end if value.is_a?(Hash) value.map do |k, v| - "#{key}.#{k} = #{v}" + "#{key}.#{k} = #{@model.quote(v)}" end.join(", ") + for_clause else "#{key} = #{@model.quote(value)}#{for_clause}" From 6d3711cc5a9d8ef8a74b5ffc75504b7b94e0f5c6 Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Wed, 24 Jun 2026 15:29:51 +0200 Subject: [PATCH 2/3] Handle nil values in nested Hash attribute updates When a nil value is passed in a Hash update (e.g. update(field: { key: nil })), quote() returns nil which interpolates as an empty string, producing invalid N1QL syntax. Fall back to 'NULL' literal in that case. Co-Authored-By: Claude Opus 4.6 --- lib/couchbase-orm/relation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/couchbase-orm/relation.rb b/lib/couchbase-orm/relation.rb index 99d8026..98186c3 100644 --- a/lib/couchbase-orm/relation.rb +++ b/lib/couchbase-orm/relation.rb @@ -217,7 +217,7 @@ def build_update(**cond) end if value.is_a?(Hash) value.map do |k, v| - "#{key}.#{k} = #{@model.quote(v)}" + "#{key}.#{k} = #{@model.quote(v) || 'NULL'}" end.join(", ") + for_clause else "#{key} = #{@model.quote(value)}#{for_clause}" From d5bdce104fafcf3e7e8c120416aa6020fe282e1e Mon Sep 17 00:00:00 2001 From: Pierre Merlin Date: Wed, 24 Jun 2026 15:44:22 +0200 Subject: [PATCH 3/3] Add test coverage for nested Hash attribute quoting in update_all Cover string values, nil values, and special character quoting in Hash-style update_all to verify the N1QL injection fix. Co-Authored-By: Claude Opus 4.6 --- spec/relation_spec.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/spec/relation_spec.rb b/spec/relation_spec.rb index f58c18c..e615809 100644 --- a/spec/relation_spec.rb +++ b/spec/relation_spec.rb @@ -357,6 +357,32 @@ def self.active expect(m3.reload.children.map(&:age)).to eq([50, 20]) end + it "should update nested hash attributes with string values" do + m1 = RelationModel.create!(age: 10, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) + m2 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 15, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) + + RelationModel.where(age: 20).update_all(child: {name: "Updated", _for: :children, _when: {child: {name: "Tom"}}}) + + expect(m1.reload.children.map(&:name)).to eq(["Tom", "Jerry"]) + expect(m2.reload.children.map(&:name)).to eq(["Updated", "Jerry"]) + end + + it "should update nested hash attributes with nil values" do + m1 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) + + RelationModel.where(age: 20).update_all(child: {name: nil, _for: :children, _when: {child: {name: "Tom"}}}) + + expect(m1.reload.children.map(&:name)).to eq([nil, "Jerry"]) + end + + it "should properly quote string values containing special characters in hash updates" do + m1 = RelationModel.create!(age: 20, children: [NestedRelationModel.new(age: 10, name: "Tom"), NestedRelationModel.new(age: 20, name: "Jerry")]) + + RelationModel.where(age: 20).update_all(child: {name: "it's a test", _for: :children, _when: {child: {name: "Tom"}}}) + + expect(m1.reload.children.map(&:name)).to eq(["it's a test", "Jerry"]) + end + it "should update nested attributes with a path in a for clause" do m1 = RelationModel.create!( pathelement: PathRelationModel.new(