Skip to content

Commit 335441c

Browse files
am0o0hmac
authored andcommitted
v4: make variable names camelCase, some inhancement, remove some duplicates
1 parent e76ed94 commit 335441c

File tree

9 files changed

+123
-67
lines changed

9 files changed

+123
-67
lines changed
Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,43 @@
11
/**
2-
* add additional steps for to_ruby method of YAML/Psych library
2+
* Provides modeling for the `YAML` and `Psych` libraries.
33
*/
44

55
private import codeql.ruby.dataflow.FlowSteps
66
private import codeql.ruby.DataFlow
77
private import codeql.ruby.ApiGraphs
88

9+
/**
10+
* A taint step related to the result of `YAML.parse` calls, or similar.
11+
*In the following example, this step will propagate taint from
12+
*`source` to `sink`:
13+
*
14+
*```rb
15+
*x = source
16+
*result = YAML.parse(x)
17+
*sink result.to_ruby # Unsafe call
18+
* ```
19+
*/
920
private class YamlParseStep extends AdditionalTaintStep {
1021
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
11-
exists(DataFlow::CallNode yaml_parser_methods |
12-
yaml_parser_methods =
13-
API::getTopLevelMember(["YAML", "Psych"]).getAMethodCall(["parse", "parse_stream"]) and
22+
exists(DataFlow::CallNode yamlParserMethod |
23+
yamlParserMethod = yamlNode().getAMethodCall(["parse", "parse_stream"]) and
1424
(
15-
pred = yaml_parser_methods.getArgument(0) or
16-
pred = yaml_parser_methods.getKeywordArgument("yaml")
25+
pred = yamlParserMethod.getArgument(0) or
26+
pred = yamlParserMethod.getKeywordArgument("yaml")
1727
) and
18-
succ = yaml_parser_methods.getAMethodCall("to_ruby")
19-
)
20-
or
21-
exists(DataFlow::CallNode yaml_parser_methods |
22-
yaml_parser_methods = API::getTopLevelMember(["YAML", "Psych"]).getAMethodCall("parse_file") and
28+
succ = yamlParserMethod.getAMethodCall("to_ruby")
29+
or
30+
yamlParserMethod = yamlNode().getAMethodCall("parse_file") and
2331
(
24-
pred = yaml_parser_methods.getArgument(0) or
25-
pred = yaml_parser_methods.getKeywordArgument("filename")
32+
pred = yamlParserMethod.getArgument(0) or
33+
pred = yamlParserMethod.getKeywordArgument("filename")
2634
) and
27-
succ = yaml_parser_methods.getAMethodCall("to_ruby")
35+
succ = yamlParserMethod.getAMethodCall("to_ruby")
2836
)
2937
}
3038
}
39+
40+
/**
41+
* YAML/Psych Top level Class member
42+
*/
43+
private API::Node yamlNode() { result = API::getTopLevelMember(["YAML", "Psych"]) }

ruby/ql/lib/codeql/ruby/security/UnsafeDeserializationCustomizations.qll

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -78,26 +78,27 @@ module UnsafeDeserialization {
7878
* An argument in a call to `YAML.unsafe_*` and `YAML.load_stream` , considered sinks
7979
* for unsafe deserialization. The `YAML` module is an alias of `Psych` in
8080
* recent versions of Ruby.
81+
* the `this = yamlNode().getAMethodCall("load").getArgument(0)` is safe
82+
* in recent versions of YAML library, so it will be removed in future.
8183
*/
8284
class YamlLoadArgument extends Sink {
8385
YamlLoadArgument() {
84-
this =
85-
API::getTopLevelMember(["YAML", "Psych"])
86-
.getAMethodCall(["unsafe_load_file", "unsafe_load", "load_stream"])
87-
.getArgument(0)
86+
this = yamlNode().getAMethodCall("load").getArgument(0)
8887
or
8988
this =
90-
API::getTopLevelMember(["YAML", "Psych"])
91-
.getAMethodCall(["unsafe_load", "load_stream"])
92-
.getKeywordArgument("yaml")
89+
yamlNode().getAMethodCall(["unsafe_load_file", "unsafe_load", "load_stream"]).getArgument(0)
9390
or
94-
this =
95-
API::getTopLevelMember(["YAML", "Psych"])
96-
.getAMethodCall("unsafe_load_file")
97-
.getKeywordArgument("filename")
91+
this = yamlNode().getAMethodCall(["unsafe_load", "load_stream"]).getKeywordArgument("yaml")
92+
or
93+
this = yamlNode().getAMethodCall("unsafe_load_file").getKeywordArgument("filename")
9894
}
9995
}
10096

97+
/**
98+
* YAML/Psych Top level Class member
99+
*/
100+
private API::Node yamlNode() { result = API::getTopLevelMember(["YAML", "Psych"]) }
101+
101102
/**
102103
* An argument in a call to `YAML.parse*`, considered sinks
103104
* for unsafe deserialization if there is a call to `to_ruby` on returned value of them,
@@ -107,9 +108,7 @@ module UnsafeDeserialization {
107108
class YamlParseArgument extends Sink {
108109
YamlParseArgument() {
109110
this =
110-
API::getTopLevelMember(["YAML", "Psych"])
111-
.getAMethodCall(["parse", "parse_stream", "parse_file"])
112-
.getAMethodCall("to_ruby")
111+
yamlNode().getAMethodCall(["parse", "parse_stream", "parse_file"]).getAMethodCall("to_ruby")
113112
}
114113
}
115114

@@ -237,29 +236,20 @@ module UnsafeDeserialization {
237236
}
238237
}
239238

240-
/**
241-
* check whether an input argument has desired "key: value" input or not.
242-
*/
243-
predicate checkkeyValue(CfgNodes::ExprNodes::PairCfgNode p, string key, string value) {
244-
p.getKey().getConstantValue().isStringlikeValue(key) and
245-
DataFlow::exprNode(p.getValue()).getALocalSource().getConstantValue().toString() = value
246-
}
247-
248239
/**
249240
* An argument in a call to `Plist.parse_xml` where the marshal is `true` (which is
250241
* the default), considered a sink for unsafe deserialization.
251242
*/
252243
class UnsafePlistParsexmlArgument extends Sink {
253244
UnsafePlistParsexmlArgument() {
254-
exists(DataFlow::CallNode plistParsexml |
255-
plistParsexml = API::getTopLevelMember("Plist").getAMethodCall("parse_xml")
245+
exists(DataFlow::CallNode plistParseXml |
246+
plistParseXml = API::getTopLevelMember("Plist").getAMethodCall("parse_xml")
256247
|
257-
this = [plistParsexml.getArgument(0), plistParsexml.getKeywordArgument("filename_or_xml")] and
258-
// Exclude calls that explicitly pass a safe mode option.
259-
checkkeyValue(plistParsexml.getArgument(1).asExpr(), "marshal", "true")
248+
this = [plistParseXml.getArgument(0), plistParseXml.getKeywordArgument("filename_or_xml")] and
249+
plistParseXml.getKeywordArgument("marshal").getConstantValue().isBoolean(true)
260250
or
261-
this = [plistParsexml.getArgument(0), plistParsexml.getKeywordArgument("filename_or_xml")] and
262-
plistParsexml.getNumberOfArguments() = 1
251+
this = [plistParseXml.getArgument(0), plistParseXml.getKeywordArgument("filename_or_xml")] and
252+
plistParseXml.getNumberOfArguments() = 1
263253
)
264254
}
265255
}

ruby/ql/lib/codeql/ruby/security/UnsafeDeserializationQuery.qll

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
private import codeql.ruby.AST
1010
private import codeql.ruby.DataFlow
1111
private import codeql.ruby.TaintTracking
12-
private import codeql.ruby.ApiGraphs
1312
import UnsafeDeserializationCustomizations
1413

1514
/**

ruby/ql/src/experimental/cwe-502/PlistUnsafeDeserialization.qhelp

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@
88
</overview>
99
<recommendation>
1010
<p>
11-
This vulnerability can be prevented by using <code>Plist.parse_xml</code>.
11+
This vulnerability in Plist can be prevented by calling <code>Plist.parse_xml FileOrXmlString, marshal: false</code>.
1212
</p>
1313
</recommendation>
1414
<example>
15-
<p>In the example below, you can see safe and unsafe call of this dangerous method that can be abused by a remote user input. You can use "marshal: false" as an arugument for <code>Plist.parse_xml</code> to use it safe.
15+
<p>In the example below, you can see safe and unsafe Plist dangerous method calls that can be abused by a remote user input. You can use "marshal: false" as an arugument for <code>Plist.parse_xml</code> to use it safe.
1616
</p>
1717
<sample src="PlistUnsafeYamlDeserialization.rb" />
1818
</example>
1919
<references>
2020
<li>
21-
Security considerations from library documentation
22-
<a href="https://github.com/patsplat/plist#label-Security+considerations">patsplat/plist</a>.
21+
Security considerations from library documentation: <a href="https://github.com/patsplat/plist#label-Security+considerations">patsplat/plist Repository</a>.
2322
</li>
2423
</references>
2524
</qhelp>

ruby/ql/src/experimental/cwe-502/PlistUnsafeDeserialization.ql

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* @problem.severity warning
77
* @security-severity 9.8
88
* @precision high
9-
* @id rb/Plist-unsafe-deserialization
9+
* @id rb/plist-unsafe-deserialization
1010
* @tags security
1111
* experimental
1212
* external/cwe/cwe-502
@@ -21,31 +21,20 @@ import codeql.ruby.security.UnsafeDeserializationCustomizations
2121

2222
abstract class PlistUnsafeSinks extends DataFlow::Node { }
2323

24-
/**
25-
* check whether an input argument has desired "key: value" input or not.
26-
* borrowed from UnsafeDeserialization module with some changes
27-
*/
28-
predicate checkkeyBalue(CfgNodes::ExprNodes::PairCfgNode p, string key, string value) {
29-
p.getKey().getConstantValue().isStringlikeValue(key) and
30-
DataFlow::exprNode(p.getValue()).getALocalSource().getConstantValue().toString() = value
31-
}
32-
3324
/**
3425
* An argument in a call to `Plist.parse_xml` where the marshal is `true` (which is
3526
* the default), considered a sink for unsafe deserialization.
36-
* borrowed from UnsafeDeserialization module with some changes
3727
*/
3828
class UnsafePlistParsexmlArgument extends PlistUnsafeSinks {
3929
UnsafePlistParsexmlArgument() {
40-
exists(DataFlow::CallNode plistParsexml |
41-
plistParsexml = API::getTopLevelMember("Plist").getAMethodCall("parse_xml")
30+
exists(DataFlow::CallNode plistParseXml |
31+
plistParseXml = API::getTopLevelMember("Plist").getAMethodCall("parse_xml")
4232
|
43-
this = [plistParsexml.getArgument(0), plistParsexml.getKeywordArgument("filename_or_xml")] and
44-
// Exclude calls that explicitly pass a safe mode option.
45-
checkkeyBalue(plistParsexml.getArgument(1).asExpr(), "marshal", "true")
33+
this = [plistParseXml.getArgument(0), plistParseXml.getKeywordArgument("filename_or_xml")] and
34+
plistParseXml.getKeywordArgument("marshal").getConstantValue().isBoolean(true)
4635
or
47-
this = [plistParsexml.getArgument(0), plistParsexml.getKeywordArgument("filename_or_xml")] and
48-
plistParsexml.getNumberOfArguments() = 1
36+
this = [plistParseXml.getArgument(0), plistParseXml.getKeywordArgument("filename_or_xml")] and
37+
plistParseXml.getNumberOfArguments() = 1
4938
)
5039
}
5140
}

ruby/ql/src/experimental/cwe-502/PlistUnsafeDeserialization.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
require 'yaml'
1+
require 'plist'
22
class UsersController < ActionController::Base
33
def example
44
# not safe
5+
config = true
56
result = Plist.parse_xml(params[:yaml_string])
7+
result = Plist.parse_xml(params[:yaml_string], marshal: config)
68
result = Plist.parse_xml(params[:yaml_string], marshal: true)
79

810
# safe
11+
config = false
912
result = Plist.parse_xml(params[:yaml_string], marshal: false)
13+
result = Plist.parse_xml(params[:yaml_string], marshal: config)
1014
end
1115
end
1216

ruby/ql/src/queries/security/cwe-502/UnsafeDeserialization.qhelp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ libraries that support it, such as the Ruby standard library's <code>JSON</code>
1717
module, ensure that the parser is configured to disable
1818
deserialization of arbitrary objects.
1919
</p>
20+
21+
<p>
22+
YAML/Psych recommendation:
23+
After Psych(YAML) 4.0.0, the load method is same as safe_load method.
24+
This vulnerability can be prevented by using YAML.load (same as <code>YAML.safe_load</code>), <code>YAML.load_file</code> (same as <code>YAML.safe_load_file</code>) instead of <code>YAML.unsafe_*</code> methods.
25+
Be careful that <code>YAML.load_stream</code> don't use safe_load method, Also Be careful the <code>to_ruby</code> method of Psych get called on a trusted parsed (<code>YAML.parse*</code>) yaml document.
26+
</p>
27+
28+
<p>
29+
This vulnerability in Plist can be prevented by calling <code>Plist.parse_xml FileOrXmlString, marshal: false</code>.
30+
</p>
2031
</recommendation>
2132

2233
<example>
@@ -27,6 +38,14 @@ on data from an HTTP request. Since these methods are capable of deserializing
2738
to arbitrary objects, this is inherently unsafe.
2839
</p>
2940
<sample src="examples/UnsafeDeserializationBad.rb"/>
41+
42+
<p>In the example below, you can see safe and unsafe methods get called by a remote user input. You can give correct authorization to users, or you can use safe methods for loading yaml documents.</p>
43+
<sample src="examples/YAMLUnsafeYamlDeserialization.rb"/>
44+
45+
<p>In the example below, you can see safe and unsafe Plist dangerous method calls that can be abused by a remote user input. You can use "marshal: false" as an arugument for <code>Plist.parse_xml</code> to use it safe.
46+
</p>
47+
<sample src="examples/PlistUnsafeYamlDeserialization.rb"/>
48+
3049
<p>
3150
Using <code>JSON.parse</code> and <code>YAML.safe_load</code> instead, as in the
3251
following example, removes the vulnerability. Similarly, calling
@@ -55,6 +74,10 @@ Ruby documentation: <a href="https://ruby-doc.org/stdlib-3.0.2/libdoc/json/rdoc/
5574
<li>
5675
Ruby documentation: <a href="https://ruby-doc.org/stdlib-3.0.2/libdoc/yaml/rdoc/YAML.html#module-YAML-label-Security">security guidance on the YAML library</a>.
5776
</li>
77+
<li>
78+
You can read that how unsafe yaml load methods can lead to code executions:
79+
<a href="https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html">Universal Deserialisation Gadget for Ruby 2.x-3.x </a>.
80+
</li>
5881
</references>
5982

6083
</qhelp>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
require 'plist'
2+
class UsersController < ActionController::Base
3+
def example
4+
# not safe
5+
config = true
6+
result = Plist.parse_xml(params[:yaml_string])
7+
result = Plist.parse_xml(params[:yaml_string], marshal: config)
8+
result = Plist.parse_xml(params[:yaml_string], marshal: true)
9+
10+
# safe
11+
config = false
12+
result = Plist.parse_xml(params[:yaml_string], marshal: false)
13+
result = Plist.parse_xml(params[:yaml_string], marshal: config)
14+
end
15+
end
16+
17+
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
require 'yaml'
2+
class UsersController < ActionController::Base
3+
def example
4+
# safe
5+
Psych.load(params[:yaml_string])
6+
Psych.load_file(params[:yaml_file])
7+
Psych.parse_stream(params[:yaml_string])
8+
Psych.parse(params[:yaml_string])
9+
Psych.parse_file(params[:yaml_file])
10+
# unsafe
11+
Psych.unsafe_load(params[:yaml_string])
12+
Psych.unsafe_load_file(params[:yaml_file])
13+
Psych.load_stream(params[:yaml_string])
14+
parse_output = Psych.parse_stream(params[:yaml_string])
15+
parse_output.to_ruby
16+
Psych.parse(params[:yaml_string]).to_ruby
17+
Psych.parse_file(params[:yaml_file]).to_ruby
18+
19+
end
20+
end
21+
22+

0 commit comments

Comments
 (0)