-
Notifications
You must be signed in to change notification settings - Fork 0
Parameterize N1QL queries for prepared-statement caching #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
158fd9c
702c88c
c2cffd3
cafe88e
b3a759e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,44 +4,67 @@ module QueryHelper | |
|
|
||
| module ClassMethods | ||
|
|
||
| def build_match(key, value) | ||
| def serialize_for_binding(value) | ||
| if value.is_a?(Array) | ||
| value.map { |v| serialize_for_binding(v) } | ||
| elsif [DateTime, Time].any? { |clazz| value.is_a?(clazz) } || (value.respond_to?(:acts_like?) && value.acts_like?(:time)) | ||
| value.iso8601(@precision || 0) | ||
| elsif value.is_a?(Date) | ||
| value.to_s | ||
| else | ||
| value | ||
| end | ||
| end | ||
|
|
||
| def bind(value, params) | ||
| if value.nil? | ||
| nil | ||
| else | ||
| params << serialize_for_binding(value) | ||
| "$#{params.length}" | ||
| end | ||
| end | ||
|
Comment on lines
+7
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two important improvement opportunities in
def serialize_for_binding(value)
if value.is_a?(Array)
value.map { |v| serialize_for_binding(v) }
elsif [DateTime, Time].any? { |clazz| value.is_a?(clazz) } || (value.respond_to?(:acts_like?) && value.acts_like?(:time))
value.iso8601(@precision || 0)
elsif value.is_a?(Date)
value.to_s
else
value
end
end
def bind(value, params)
if value.nil?
nil
else
params << serialize_for_binding(value)
"$#{params.length}"
end
end |
||
|
|
||
| def build_match(key, value, params: nil) | ||
| use_is_null = self.properties_always_exists_in_document | ||
| key = "meta().id" if key.to_s == "id" | ||
| resolve = ->(v) { params ? bind(v, params) : quote(v) } | ||
| case | ||
| when value.nil? && use_is_null | ||
| "#{key} IS NULL" | ||
| when value.nil? && !use_is_null | ||
| "#{key} IS NOT VALUED" | ||
| when value.is_a?(Hash) && attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array) | ||
| "any #{key.to_s.singularize} in #{key} satisfies (#{build_match_hash("#{key.to_s.singularize}", value)}) end" | ||
| "any #{key.to_s.singularize} in #{key} satisfies (#{build_match_hash("#{key.to_s.singularize}", value, params: params)}) end" | ||
| when value.is_a?(Hash) && !attribute_types[key.to_s].is_a?(CouchbaseOrm::Types::Array) | ||
| build_match_hash(key, value) | ||
| build_match_hash(key, value, params: params) | ||
| when value.is_a?(Array) && value.include?(nil) | ||
| "(#{build_match(key, nil)} OR #{build_match(key, value.compact)})" | ||
| "(#{build_match(key, nil, params: params)} OR #{build_match(key, value.compact, params: params)})" | ||
| when value.is_a?(Array) | ||
| "#{key} IN #{quote(value)}" | ||
| "#{key} IN #{resolve.call(value)}" | ||
| when value.is_a?(Range) | ||
| build_match_range(key, value) | ||
| build_match_range(key, value, params: params) | ||
| else | ||
| "#{key} = #{quote(value)}" | ||
| "#{key} = #{resolve.call(value)}" | ||
| end | ||
| end | ||
|
|
||
| def build_match_hash(key, value) | ||
| def build_match_hash(key, value, params: nil) | ||
| matches = [] | ||
| resolve = ->(v) { params ? bind(v, params) : quote(v) } | ||
| value.each do |k, v| | ||
| case k | ||
| when :_gt | ||
| matches << "#{key} > #{quote(v)}" | ||
| matches << "#{key} > #{resolve.call(v)}" | ||
| when :_gte | ||
| matches << "#{key} >= #{quote(v)}" | ||
| matches << "#{key} >= #{resolve.call(v)}" | ||
| when :_lt | ||
| matches << "#{key} < #{quote(v)}" | ||
| matches << "#{key} < #{resolve.call(v)}" | ||
| when :_lte | ||
| matches << "#{key} <= #{quote(v)}" | ||
| matches << "#{key} <= #{resolve.call(v)}" | ||
| when :_ne | ||
| matches << "#{key} != #{quote(v)}" | ||
| matches << "#{key} != #{resolve.call(v)}" | ||
|
|
||
| # TODO v2 | ||
| # when :_in | ||
| # matches << "#{key} IN #{quote(v)}" | ||
|
|
@@ -65,7 +88,7 @@ def build_match_hash(key, value) | |
| # matches << "#{key} MATCH #{quote(v)}" | ||
| # when :_nmatch | ||
| # matches << "#{key} NOT MATCH #{quote(v)}" | ||
|
|
||
| # TODO v3 | ||
| # when :_any | ||
| # matches << "#{key} ANY #{quote(v)}" | ||
|
|
@@ -80,39 +103,41 @@ def build_match_hash(key, value) | |
| #when :_nwithin | ||
| # matches << "#{key} NOT WITHIN #{quote(v)}" | ||
| else | ||
| matches << build_match("#{key}.#{k}", v) | ||
| matches << build_match("#{key}.#{k}", v, params: params) | ||
| end | ||
| end | ||
|
|
||
| matches.join(" AND ") | ||
| end | ||
|
|
||
| def build_match_range(key, value) | ||
| def build_match_range(key, value, params: nil) | ||
| resolve = ->(v) { params ? bind(v, params) : quote(v) } | ||
| matches = [] | ||
| matches << "#{key} >= #{quote(value.begin)}" | ||
| matches << "#{key} >= #{resolve.call(value.begin)}" | ||
| if value.exclude_end? | ||
| matches << "#{key} < #{quote(value.end)}" | ||
| matches << "#{key} < #{resolve.call(value.end)}" | ||
| else | ||
| matches << "#{key} <= #{quote(value.end)}" | ||
| matches << "#{key} <= #{resolve.call(value.end)}" | ||
| end | ||
| matches.join(" AND ") | ||
| end | ||
|
|
||
|
|
||
| def build_not_match(key, value) | ||
| def build_not_match(key, value, params: nil) | ||
| use_is_null = self.properties_always_exists_in_document | ||
| key = "meta().id" if key.to_s == "id" | ||
| resolve = ->(v) { params ? bind(v, params) : quote(v) } | ||
| case | ||
| when value.nil? && use_is_null | ||
| "#{key} IS NOT NULL" | ||
| when value.nil? && !use_is_null | ||
| "#{key} IS VALUED" | ||
| when value.is_a?(Array) && value.include?(nil) | ||
| "(#{build_not_match(key, nil)} AND #{build_not_match(key, value.compact)})" | ||
| "(#{build_not_match(key, nil, params: params)} AND #{build_not_match(key, value.compact, params: params)})" | ||
| when value.is_a?(Array) | ||
| "#{key} NOT IN #{quote(value)}" | ||
| "#{key} NOT IN #{resolve.call(value)}" | ||
| else | ||
| "#{key} != #{quote(value)}" | ||
| "#{key} != #{resolve.call(value)}" | ||
| end | ||
| end | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
build_update_with_params, whenvalueis aHash(used for updating nested attributes), the values are directly interpolated into the query string as#{v}without being parameterized or quoted:This not only defeats prepared-statement caching for nested attribute updates but also introduces a N1QL injection vulnerability if user-controlled input is passed to nested attributes.
To fix this, we should parameterize these values using
@model.bind(v, params)as well: