-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Ruby: Add YAML unsafe deserialization sinks #13307
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
Conversation
Merge the experimental YAMLUnsafeDeserialization and PlistUnsafeDeserialization queries into the generate UnsafeDeserialization query in the default suite. These queries look for some specific sinks that we now find in the general query. Also apply some small code and comment refactors.
QHelp previews: ruby/ql/src/queries/security/cwe-502/UnsafeDeserialization.qhelpDeserialization of user-controlled dataDeserializing untrusted data using any method that allows the construction of arbitrary objects is easily exploitable and, in many cases, allows an attacker to execute arbitrary code. RecommendationAvoid deserialization of untrusted data if possible. If the architecture permits it, use serialization formats that cannot represent arbitrary objects. For libraries that support it, such as the Ruby standard library's If deserializing an untrusted YAML document using the To safely deserialize Property List files using the ExampleThe following example calls the require 'json'
require 'yaml'
require 'oj'
class UserController < ActionController::Base
def marshal_example
data = Base64.decode64 params[:data]
object = Marshal.load data
# ...
end
def json_example
object = JSON.load params[:json]
# ...
end
def yaml_example
object = YAML.load params[:yaml]
# ...
end
def oj_example
object = Oj.load params[:json]
# ...
end
end Using require 'json'
class UserController < ActionController::Base
def safe_json_example
object = JSON.parse params[:json]
# ...
end
def safe_yaml_example
object = YAML.safe_load params[:yaml]
# ...
end
def safe_oj_example
object = Oj.load params[:yaml], { mode: :strict }
# or
object = Oj.safe_load params[:yaml]
# ...
end
end References
|
* sink result.to_ruby # Unsafe call | ||
* ``` | ||
*/ | ||
private class YamlParseStep extends AdditionalTaintStep { |
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.
according to #12301 (comment)
I didn't write following predicate correctly, I think it should be two separate steps:
1.
x = source
sink = YAML.parse(x)
sink = source.to_ruby
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.
I'm not sure about that - the second step should only apply when the return value of YAML.parse(x)
flows to source
, right? We could add to_ruby
as a global taint-preserving step but I think that could result in false positives where some other class defines a to_ruby
method. I've requested a review from others in the team; we'll see what they say.
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.
yes second one can have FP,
please consider the following example too, if you like I can try to write an additional step for such a pattern too:
https://github.com/Shopify/krane/blob/0e1a3a0651ca4a0b234722ea98dae74ca8c65351/lib/krane/renderer.rb#L61-L67
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.
I think adding a separate taint step for the to_ruby
method makes sense. However, let's leave that for a followup pull request in which we can investigate the impact a bit. I would expect to_ruby
methods from other libraries to be mostly taint-preserving too, but we might be mistaken.
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.
Hi, I want to do some changes, can I commit here or I should wait for merge and open another pull request after merge?
the changes summary:
new examples that pushed me to improve the query: parsed_yaml = Psych.parse(params[:yaml_string])
parsed_yaml.children.each do |child|
puts child.to_ruby
end
Psych.parse_stream(params[:yaml_string]) do |document|
puts document.to_ruby
end
# https://github.com/Shopify/krane/blob/0e1a3a0651ca4a0b234722ea98dae74ca8c65351/lib/krane/renderer.rb#L61-L67
parsed_yaml.children.first.to_ruby
# https://github.com/work-design/rails_extend/blob/c013aef0b7e769e37010464b0d6c2db8e83601d8/lib/rails_extend/yaml.rb#L10-L13
parsed_yaml = Psych.parse_stream(params[:yaml_string])
content = parsed_yaml.children[0].children[0].children
parsed = parsed_yaml.to_ruby[0]
parsed = content.to_ruby[0]
Psych.parse(params[:yaml_string]).children[0].to_ruby private class YamlParseStep extends AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(API::Node yamlParserMethod |
succ = yamlParserMethod.getASuccessor*().getMethod("to_ruby").asSource() and
(
yamlParserMethod = yamlNode().getMethod(["parse", "parse_stream"]) and
pred =
[yamlParserMethod.getParameter(0), yamlParserMethod.getKeywordParameter("yaml")].asSink()
or
yamlParserMethod = yamlNode().getMethod("parse_file") and
pred =
[yamlParserMethod.getParameter(0), yamlParserMethod.getKeywordParameter("filename")]
.asSink()
)
)
}
} the related sinks: class YamlParseArgument extends Sink {
YamlParseArgument() {
this =
yamlNode()
.getMethod(["parse", "parse_stream", "parse_file"])
.getASuccessor*()
.getAMethodCall("to_ruby")
}
} |
also we can solve the FP of this by following which I think we can find any private class YamlParseStep extends AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(API::Node yamlParserMethod |
succ = yamlParserMethod.getReturn().asSource() and
(
yamlParserMethod = yamlNode().getMethod(["parse", "parse_stream"]) and
pred =
[yamlParserMethod.getParameter(0), yamlParserMethod.getKeywordParameter("yaml")].asSink()
or
yamlParserMethod = yamlNode().getMethod("parse_file") and
pred =
[yamlParserMethod.getParameter(0), yamlParserMethod.getKeywordParameter("filename")]
.asSink()
)
)
or
exists(API::Node yamlParserMethod |
succ =
[
yamlParserMethod.getASuccessor*().getMethod("to_ruby").getReturn().asSource(),
yamlParserMethod
.getASuccessor*()
.getMethod("to_ruby")
.getReturn()
.getAnElement()
.asSource()
] and
yamlParserMethod = yamlNode().getMethod(["parse", "parse_stream", "parse_file"]) and
pred = yamlParserMethod.getReturn().asSource()
)
}
} |
The changes you propose sound like a good idea. However, let's merge this first and make a new pull request for further improvements. Thanks! |
This is a duplicate of #12301 which I've opened to make some changes before merging (the original is from a contributor's fork).