mirror of
https://github.com/github/codeql.git
synced 2026-05-01 19:55:15 +02:00
Merge branch 'main' into delOldDeps
This commit is contained in:
BIN
ruby/Cargo.lock
generated
BIN
ruby/Cargo.lock
generated
Binary file not shown.
@@ -11,8 +11,21 @@ file_types:
|
||||
- name: ruby
|
||||
display_name: Ruby files
|
||||
extensions:
|
||||
- .rb
|
||||
- .rb
|
||||
- name: erb
|
||||
display_name: Ruby templates
|
||||
extensions:
|
||||
- .erb
|
||||
- .erb
|
||||
options:
|
||||
trap:
|
||||
title: Options pertaining to TRAP.
|
||||
type: object
|
||||
properties:
|
||||
compression:
|
||||
title: Controls compression for the TRAP files written by the extractor.
|
||||
description: >
|
||||
This option is only intended for use in debugging the extractor. Accepted
|
||||
values are 'gzip' (the default, to write gzip-compressed TRAP) and 'none'
|
||||
(to write uncompressed TRAP).
|
||||
type: string
|
||||
pattern: "^(none|gzip)$"
|
||||
|
||||
@@ -5,4 +5,3 @@ class AstNode extends @ruby_ast_node {
|
||||
from AstNode ruby_do_block, AstNode body, int index, AstNode child
|
||||
where ruby_do_block_body(ruby_do_block, body) and ruby_body_statement_child(body, index, child)
|
||||
select ruby_do_block, index, child
|
||||
|
||||
|
||||
@@ -5,4 +5,3 @@ class AstNode extends @ruby_ast_node {
|
||||
from AstNode ruby_module, AstNode body, int index, AstNode child
|
||||
where ruby_module_body(ruby_module, body) and ruby_body_statement_child(body, index, child)
|
||||
select ruby_module, index, child
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ class AstNode extends @ruby_ast_node {
|
||||
}
|
||||
|
||||
from AstNode ruby_singleton_class, AstNode body, int index, AstNode child
|
||||
where ruby_singleton_class_body(ruby_singleton_class, body) and ruby_body_statement_child(body, index, child)
|
||||
where
|
||||
ruby_singleton_class_body(ruby_singleton_class, body) and
|
||||
ruby_body_statement_child(body, index, child)
|
||||
select ruby_singleton_class, index, child
|
||||
|
||||
|
||||
@@ -214,6 +214,14 @@ fn longest_backtick_sequence_length(text: &str) -> usize {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// An argument of a diagnostic message format string.
|
||||
/// A message argument is either a "code" snippet or a link.
|
||||
pub enum MessageArg<'a> {
|
||||
Code(&'a str),
|
||||
Link(&'a str, &'a str),
|
||||
}
|
||||
|
||||
impl DiagnosticMessage {
|
||||
pub fn full_error_message(&self) -> String {
|
||||
match &self.location {
|
||||
@@ -236,26 +244,39 @@ impl DiagnosticMessage {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn message(&mut self, text: &str, args: &[&str]) -> &mut Self {
|
||||
pub fn message(&mut self, text: &str, args: &[MessageArg]) -> &mut Self {
|
||||
let parts = text.split("{}");
|
||||
let args = args.iter().chain(std::iter::repeat(&""));
|
||||
let mut plain = String::with_capacity(2 * text.len());
|
||||
let mut markdown = String::with_capacity(2 * text.len());
|
||||
for (p, a) in parts.zip(args) {
|
||||
for (i, p) in parts.enumerate() {
|
||||
plain.push_str(p);
|
||||
plain.push_str(a);
|
||||
markdown.push_str(p);
|
||||
if a.len() > 0 {
|
||||
let count = longest_backtick_sequence_length(a) + 1;
|
||||
markdown.push_str(&"`".repeat(count));
|
||||
if count > 1 {
|
||||
markdown.push_str(" ");
|
||||
match args.get(i) {
|
||||
Some(MessageArg::Code(t)) => {
|
||||
plain.push_str(t);
|
||||
if t.len() > 0 {
|
||||
let count = longest_backtick_sequence_length(t) + 1;
|
||||
markdown.push_str(&"`".repeat(count));
|
||||
if count > 1 {
|
||||
markdown.push_str(" ");
|
||||
}
|
||||
markdown.push_str(t);
|
||||
if count > 1 {
|
||||
markdown.push_str(" ");
|
||||
}
|
||||
markdown.push_str(&"`".repeat(count));
|
||||
}
|
||||
}
|
||||
markdown.push_str(a);
|
||||
if count > 1 {
|
||||
markdown.push_str(" ");
|
||||
Some(MessageArg::Link(text, url)) => {
|
||||
plain.push_str(text);
|
||||
self.help_link(url);
|
||||
markdown.push_str("[");
|
||||
markdown.push_str(text);
|
||||
markdown.push_str("](");
|
||||
markdown.push_str(url);
|
||||
markdown.push_str(")");
|
||||
}
|
||||
markdown.push_str(&"`".repeat(count));
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
self.text(&plain);
|
||||
@@ -323,14 +344,17 @@ fn test_message() {
|
||||
let mut m = DiagnosticLoggers::new("foo")
|
||||
.logger()
|
||||
.new_entry("id", "name");
|
||||
m.message("hello: {}", &["hello"]);
|
||||
m.message("hello: {}", &[MessageArg::Code("hello")]);
|
||||
assert_eq!("hello: hello", m.plaintext_message);
|
||||
assert_eq!("hello: `hello`", m.markdown_message);
|
||||
|
||||
let mut m = DiagnosticLoggers::new("foo")
|
||||
.logger()
|
||||
.new_entry("id", "name");
|
||||
m.message("hello with backticks: {}", &["oh `hello`!"]);
|
||||
m.message(
|
||||
"hello with backticks: {}",
|
||||
&[MessageArg::Code("oh `hello`!")],
|
||||
);
|
||||
assert_eq!("hello with backticks: oh `hello`!", m.plaintext_message);
|
||||
assert_eq!(
|
||||
"hello with backticks: `` oh `hello`! ``",
|
||||
|
||||
@@ -277,7 +277,7 @@ impl<'a> Visitor<'a> {
|
||||
fn record_parse_error_for_node(
|
||||
&mut self,
|
||||
message: &str,
|
||||
args: &[&str],
|
||||
args: &[diagnostics::MessageArg],
|
||||
node: Node,
|
||||
status_page: bool,
|
||||
) {
|
||||
@@ -294,7 +294,7 @@ impl<'a> Visitor<'a> {
|
||||
.diagnostics_writer
|
||||
.new_entry("parse-error", "Parse error");
|
||||
&mesg
|
||||
.severity(diagnostics::Severity::Error)
|
||||
.severity(diagnostics::Severity::Warning)
|
||||
.location(self.path, start_line, start_column, end_line, end_column)
|
||||
.message(message, args);
|
||||
if status_page {
|
||||
@@ -306,8 +306,8 @@ impl<'a> Visitor<'a> {
|
||||
fn enter_node(&mut self, node: Node) -> bool {
|
||||
if node.is_missing() {
|
||||
self.record_parse_error_for_node(
|
||||
"A parse error occurred (expected {} symbol). Check the syntax of the file using the {} command. If the file is invalid, correct the error or exclude the file from analysis.",
|
||||
&[node.kind(), "ruby -c"],
|
||||
"A parse error occurred (expected {} symbol). Check the syntax of the file. If the file is invalid, correct the error or {} the file from analysis.",
|
||||
&[diagnostics::MessageArg::Code(node.kind()), diagnostics::MessageArg::Link("exclude", "https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning")],
|
||||
node,
|
||||
true,
|
||||
);
|
||||
@@ -315,8 +315,8 @@ impl<'a> Visitor<'a> {
|
||||
}
|
||||
if node.is_error() {
|
||||
self.record_parse_error_for_node(
|
||||
"A parse error occurred. Check the syntax of the file using the {} command. If the file is invalid, correct the error or exclude the file from analysis.",
|
||||
&["ruby -c"],
|
||||
"A parse error occurred. Check the syntax of the file. If the file is invalid, correct the error or {} the file from analysis.",
|
||||
&[diagnostics::MessageArg::Link("exclude", "https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning")],
|
||||
node,
|
||||
true,
|
||||
);
|
||||
@@ -405,9 +405,12 @@ impl<'a> Visitor<'a> {
|
||||
loc,
|
||||
self.diagnostics_writer
|
||||
.new_entry("parse-error", "Parse error")
|
||||
.severity(diagnostics::Severity::Error)
|
||||
.severity(diagnostics::Severity::Warning)
|
||||
.location(self.path, start_line, start_column, end_line, end_column)
|
||||
.message("Unknown table type: {}", &[node.kind()]),
|
||||
.message(
|
||||
"Unknown table type: {}",
|
||||
&[diagnostics::MessageArg::Code(node.kind())],
|
||||
),
|
||||
);
|
||||
|
||||
valid = false;
|
||||
@@ -458,10 +461,10 @@ impl<'a> Visitor<'a> {
|
||||
self.record_parse_error_for_node(
|
||||
"Type mismatch for field {}::{} with type {} != {}",
|
||||
&[
|
||||
node.kind(),
|
||||
child_node.field_name.unwrap_or("child"),
|
||||
&format!("{:?}", child_node.type_name),
|
||||
&format!("{:?}", field.type_info),
|
||||
diagnostics::MessageArg::Code(node.kind()),
|
||||
diagnostics::MessageArg::Code(child_node.field_name.unwrap_or("child")),
|
||||
diagnostics::MessageArg::Code(&format!("{:?}", child_node.type_name)),
|
||||
diagnostics::MessageArg::Code(&format!("{:?}", field.type_info)),
|
||||
],
|
||||
*node,
|
||||
false,
|
||||
@@ -471,9 +474,9 @@ impl<'a> Visitor<'a> {
|
||||
self.record_parse_error_for_node(
|
||||
"Value for unknown field: {}::{} and type {}",
|
||||
&[
|
||||
node.kind(),
|
||||
&child_node.field_name.unwrap_or("child"),
|
||||
&format!("{:?}", child_node.type_name),
|
||||
diagnostics::MessageArg::Code(node.kind()),
|
||||
diagnostics::MessageArg::Code(&child_node.field_name.unwrap_or("child")),
|
||||
diagnostics::MessageArg::Code(&format!("{:?}", child_node.type_name)),
|
||||
],
|
||||
*node,
|
||||
false,
|
||||
@@ -512,7 +515,10 @@ impl<'a> Visitor<'a> {
|
||||
if !*has_index && index > 0 {
|
||||
self.record_parse_error_for_node(
|
||||
"Too many values for field: {}::{}",
|
||||
&[node.kind(), table_name],
|
||||
&[
|
||||
diagnostics::MessageArg::Code(node.kind()),
|
||||
diagnostics::MessageArg::Code(table_name),
|
||||
],
|
||||
*node,
|
||||
false,
|
||||
);
|
||||
@@ -629,8 +635,11 @@ fn location_for(visitor: &mut Visitor, n: Node) -> (usize, usize, usize, usize)
|
||||
.diagnostics_writer
|
||||
.new_entry("internal-error", "Internal error")
|
||||
.message(
|
||||
"Cannot correct end column value: end_byte index {} is not in range [1,{}]",
|
||||
&[&index.to_string(), &source.len().to_string()],
|
||||
"Cannot correct end column value: end_byte index {} is not in range [1,{}].",
|
||||
&[
|
||||
diagnostics::MessageArg::Code(&index.to_string()),
|
||||
diagnostics::MessageArg::Code(&source.len().to_string()),
|
||||
],
|
||||
)
|
||||
.severity(diagnostics::Severity::Error),
|
||||
);
|
||||
|
||||
@@ -74,7 +74,10 @@ fn main() -> std::io::Result<()> {
|
||||
main_thread_logger.write(
|
||||
main_thread_logger
|
||||
.new_entry("configuration-error", "Configuration error")
|
||||
.message("{}; defaulting to 1 thread.", &[&e])
|
||||
.message(
|
||||
"{}; defaulting to 1 thread.",
|
||||
&[diagnostics::MessageArg::Code(&e)],
|
||||
)
|
||||
.severity(diagnostics::Severity::Warning),
|
||||
);
|
||||
1
|
||||
@@ -89,18 +92,19 @@ fn main() -> std::io::Result<()> {
|
||||
"threads"
|
||||
}
|
||||
);
|
||||
let trap_compression = match trap::Compression::from_env("CODEQL_RUBY_TRAP_COMPRESSION") {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
main_thread_logger.write(
|
||||
main_thread_logger
|
||||
.new_entry("configuration-error", "Configuration error")
|
||||
.message("{}; using gzip.", &[&e])
|
||||
.severity(diagnostics::Severity::Warning),
|
||||
);
|
||||
trap::Compression::Gzip
|
||||
}
|
||||
};
|
||||
let trap_compression =
|
||||
match trap::Compression::from_env("CODEQL_EXTRACTOR_RUBY_OPTION_TRAP_COMPRESSION") {
|
||||
Ok(x) => x,
|
||||
Err(e) => {
|
||||
main_thread_logger.write(
|
||||
main_thread_logger
|
||||
.new_entry("configuration-error", "Configuration error")
|
||||
.message("{}; using gzip.", &[diagnostics::MessageArg::Code(&e)])
|
||||
.severity(diagnostics::Severity::Warning),
|
||||
);
|
||||
trap::Compression::Gzip
|
||||
}
|
||||
};
|
||||
drop(main_thread_logger);
|
||||
rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(num_threads)
|
||||
@@ -203,11 +207,15 @@ fn main() -> std::io::Result<()> {
|
||||
)
|
||||
.file(&path.to_string_lossy())
|
||||
.message(
|
||||
"Could not decode the file contents as {}: {}. The contents of the file must match the character encoding specified in the {} directive.",
|
||||
&[&encoding_name, &msg, "encoding:"],
|
||||
"Could not decode the file contents as {}: {}. The contents of the file must match the character encoding specified in the {} {}.",
|
||||
&[
|
||||
diagnostics::MessageArg::Code(&encoding_name),
|
||||
diagnostics::MessageArg::Code(&msg),
|
||||
diagnostics::MessageArg::Code("encoding:"),
|
||||
diagnostics::MessageArg::Link("directive", "https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive")
|
||||
],
|
||||
)
|
||||
.status_page()
|
||||
.help_link("https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive")
|
||||
.severity(diagnostics::Severity::Warning),
|
||||
);
|
||||
}
|
||||
@@ -219,11 +227,14 @@ fn main() -> std::io::Result<()> {
|
||||
.new_entry("unknown-character-encoding", "Unknown character encoding")
|
||||
.file(&path.to_string_lossy())
|
||||
.message(
|
||||
"Unknown character encoding {} in {} directive.",
|
||||
&[&encoding_name, "#encoding:"],
|
||||
"Unknown character encoding {} in {} {}.",
|
||||
&[
|
||||
diagnostics::MessageArg::Code(&encoding_name),
|
||||
diagnostics::MessageArg::Code("#encoding:"),
|
||||
diagnostics::MessageArg::Link("directive", "https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive")
|
||||
],
|
||||
)
|
||||
.status_page()
|
||||
.help_link("https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive")
|
||||
.severity(diagnostics::Severity::Warning),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
4 %%% 5
|
||||
|
||||
if 1; 2
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"helpLinks": [
|
||||
"https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning"
|
||||
],
|
||||
"location": {
|
||||
"endColumn": 5,
|
||||
"endLine": 1,
|
||||
"file": "<test-root-directory>/bad.rb",
|
||||
"startColumn": 4,
|
||||
"startLine": 1
|
||||
},
|
||||
"markdownMessage": "A parse error occurred. Check the syntax of the file. If the file is invalid, correct the error or [exclude](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning) the file from analysis.",
|
||||
"plaintextMessage": "A parse error occurred. Check the syntax of the file. If the file is invalid, correct the error or exclude the file from analysis.",
|
||||
"severity": "Warning",
|
||||
"source": {
|
||||
"extractorName": "ruby",
|
||||
"id": "ruby/parse-error",
|
||||
"name": "Parse error"
|
||||
},
|
||||
"visibility": {
|
||||
"statusPage": true
|
||||
}
|
||||
}
|
||||
{
|
||||
"helpLinks": [
|
||||
"https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning"
|
||||
],
|
||||
"location": {
|
||||
"endColumn": 7,
|
||||
"endLine": 3,
|
||||
"file": "<test-root-directory>/bad.rb",
|
||||
"startColumn": 8,
|
||||
"startLine": 3
|
||||
},
|
||||
"markdownMessage": "A parse error occurred (expected `end` symbol). Check the syntax of the file. If the file is invalid, correct the error or [exclude](https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/customizing-code-scanning) the file from analysis.",
|
||||
"plaintextMessage": "A parse error occurred (expected end symbol). Check the syntax of the file. If the file is invalid, correct the error or exclude the file from analysis.",
|
||||
"severity": "Warning",
|
||||
"source": {
|
||||
"extractorName": "ruby",
|
||||
"id": "ruby/parse-error",
|
||||
"name": "Parse error"
|
||||
},
|
||||
"visibility": {
|
||||
"statusPage": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
from create_database_utils import *
|
||||
from diagnostics_test_utils import *
|
||||
|
||||
run_codeql_database_create([], lang="ruby", runFunction = runSuccessfully, db = None)
|
||||
|
||||
check_diagnostics()
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"helpLinks": [
|
||||
"https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive"
|
||||
],
|
||||
"location": {
|
||||
"file": "<test-root-directory>/encoding.rb"
|
||||
},
|
||||
"markdownMessage": "Unknown character encoding `silly` in `#encoding:` [directive](https://docs.ruby-lang.org/en/master/syntax/comments_rdoc.html#label-encoding+Directive).",
|
||||
"plaintextMessage": "Unknown character encoding silly in #encoding: directive.",
|
||||
"severity": "Warning",
|
||||
"source": {
|
||||
"extractorName": "ruby",
|
||||
"id": "ruby/unknown-character-encoding",
|
||||
"name": "Unknown character encoding"
|
||||
},
|
||||
"visibility": {
|
||||
"statusPage": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# encoding: silly
|
||||
|
||||
def f
|
||||
puts "hello"
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
import os
|
||||
from create_database_utils import *
|
||||
from diagnostics_test_utils import *
|
||||
|
||||
run_codeql_database_create([], lang="ruby", runFunction = runSuccessfully, db = None)
|
||||
|
||||
check_diagnostics()
|
||||
3
ruby/ql/integration-tests/all-platforms/qlpack.yml
Normal file
3
ruby/ql/integration-tests/all-platforms/qlpack.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
codeql/ruby-all: '*'
|
||||
codeql/ruby-queries: '*'
|
||||
@@ -1,3 +1,11 @@
|
||||
## 0.5.4
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Flow is now tracked between ActionController `before_filter` and `after_filter` callbacks and their associated action methods.
|
||||
* Calls to `ApplicationController#render` and `ApplicationController::Renderer#render` are recognized as Rails rendering calls.
|
||||
* Support for [Twirp framework](https://twitchtv.github.io/twirp/docs/intro.html).
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Calls to `ApplicationController#render` and `ApplicationController::Renderer#render` are recognized as Rails rendering calls.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Support for [Twirp framework](https://twitchtv.github.io/twirp/docs/intro.html).
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* Flow is now tracked between ActionController `before_filter` and `after_filter` callbacks and their associated action methods.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
category: majorAnalysis
|
||||
---
|
||||
* The main data flow and taint tracking APIs have been changed. The old APIs
|
||||
remain in place for now and translate to the new through a
|
||||
backwards-compatible wrapper. If multiple configurations are in scope
|
||||
simultaneously, then this may affect results slightly. The new API is quite
|
||||
similar to the old, but makes use of a configuration module instead of a
|
||||
configuration class.
|
||||
4
ruby/ql/lib/change-notes/2023-03-09-parse-error.md
Normal file
4
ruby/ql/lib/change-notes/2023-03-09-parse-error.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: minorAnalysis
|
||||
---
|
||||
* The severity of parse errors was reduced to warning (previously error).
|
||||
7
ruby/ql/lib/change-notes/released/0.5.4.md
Normal file
7
ruby/ql/lib/change-notes/released/0.5.4.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## 0.5.4
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
* Flow is now tracked between ActionController `before_filter` and `after_filter` callbacks and their associated action methods.
|
||||
* Calls to `ApplicationController#render` and `ApplicationController::Renderer#render` are recognized as Rails rendering calls.
|
||||
* Support for [Twirp framework](https://twitchtv.github.io/twirp/docs/intro.html).
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.5.3
|
||||
lastReleaseVersion: 0.5.4
|
||||
|
||||
@@ -212,7 +212,8 @@ module FileSystemWriteAccess {
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `FileSystemPermissionModification::Range` instead.
|
||||
*/
|
||||
class FileSystemPermissionModification extends DataFlow::Node instanceof FileSystemPermissionModification::Range {
|
||||
class FileSystemPermissionModification extends DataFlow::Node instanceof FileSystemPermissionModification::Range
|
||||
{
|
||||
/**
|
||||
* Gets an argument to this permission modification that is interpreted as a
|
||||
* set of permissions.
|
||||
@@ -468,7 +469,8 @@ module Http {
|
||||
}
|
||||
}
|
||||
|
||||
private class RequestInputAccessAsRemoteFlowSource extends RemoteFlowSource::Range instanceof RequestInputAccess {
|
||||
private class RequestInputAccessAsRemoteFlowSource extends RemoteFlowSource::Range instanceof RequestInputAccess
|
||||
{
|
||||
override string getSourceType() { result = this.(RequestInputAccess).getSourceType() }
|
||||
}
|
||||
|
||||
@@ -957,7 +959,8 @@ module Path {
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CookieSecurityConfigurationSetting::Range` instead.
|
||||
*/
|
||||
class CookieSecurityConfigurationSetting extends DataFlow::Node instanceof CookieSecurityConfigurationSetting::Range {
|
||||
class CookieSecurityConfigurationSetting extends DataFlow::Node instanceof CookieSecurityConfigurationSetting::Range
|
||||
{
|
||||
/**
|
||||
* Gets a description of how this cookie setting may weaken application security.
|
||||
* This predicate has no results if the setting is considered to be safe.
|
||||
@@ -1037,7 +1040,8 @@ module Cryptography {
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `CryptographicOperation::Range` instead.
|
||||
*/
|
||||
class CryptographicOperation extends SC::CryptographicOperation instanceof CryptographicOperation::Range {
|
||||
class CryptographicOperation extends SC::CryptographicOperation instanceof CryptographicOperation::Range
|
||||
{
|
||||
/** DEPRECATED: Use `getAlgorithm().isWeak() or getBlockMode().isWeak()` instead */
|
||||
deprecated predicate isWeak() { super.isWeak() }
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ import codeql.Locations
|
||||
* global (inter-procedural) data flow analyses.
|
||||
*/
|
||||
module DataFlow {
|
||||
import codeql.ruby.dataflow.internal.DataFlowImpl
|
||||
import codeql.ruby.dataflow.internal.DataFlow
|
||||
import codeql.ruby.dataflow.internal.DataFlowImpl1
|
||||
}
|
||||
|
||||
@@ -47,6 +47,6 @@ class Diagnostic extends @diagnostic {
|
||||
}
|
||||
|
||||
/** A diagnostic relating to a particular error in extracting a file. */
|
||||
class ExtractionError extends Diagnostic, @diagnostic_error {
|
||||
class ExtractionError extends Diagnostic {
|
||||
ExtractionError() { this.getTag() = "parse_error" }
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
* global (inter-procedural) taint-tracking analyses.
|
||||
*/
|
||||
module TaintTracking {
|
||||
import codeql.ruby.dataflow.internal.tainttracking1.TaintTracking
|
||||
import codeql.ruby.dataflow.internal.tainttracking1.TaintTrackingImpl
|
||||
}
|
||||
|
||||
@@ -199,7 +199,8 @@ class StringTextComponent extends StringComponent instanceof StringTextComponent
|
||||
/**
|
||||
* An escape sequence component of a string or string-like literal.
|
||||
*/
|
||||
class StringEscapeSequenceComponent extends StringComponent instanceof StringEscapeSequenceComponentImpl {
|
||||
class StringEscapeSequenceComponent extends StringComponent instanceof StringEscapeSequenceComponentImpl
|
||||
{
|
||||
final override string getAPrimaryQlClass() { result = "StringEscapeSequenceComponent" }
|
||||
|
||||
/** Gets the text of this component as it appears in the source code. */
|
||||
@@ -209,7 +210,8 @@ class StringEscapeSequenceComponent extends StringComponent instanceof StringEsc
|
||||
/**
|
||||
* An interpolation expression component of a string or string-like literal.
|
||||
*/
|
||||
class StringInterpolationComponent extends StringComponent, StmtSequence instanceof StringInterpolationComponentImpl {
|
||||
class StringInterpolationComponent extends StringComponent, StmtSequence instanceof StringInterpolationComponentImpl
|
||||
{
|
||||
private Ruby::Interpolation g;
|
||||
|
||||
StringInterpolationComponent() { this = TStringInterpolationComponentNonRegexp(g) }
|
||||
@@ -249,14 +251,16 @@ class RegExpTextComponent extends RegExpComponent instanceof RegExpTextComponent
|
||||
/**
|
||||
* An escape sequence component of a regex literal.
|
||||
*/
|
||||
class RegExpEscapeSequenceComponent extends RegExpComponent instanceof RegExpEscapeSequenceComponentImpl {
|
||||
class RegExpEscapeSequenceComponent extends RegExpComponent instanceof RegExpEscapeSequenceComponentImpl
|
||||
{
|
||||
final override string getAPrimaryQlClass() { result = "RegExpEscapeSequenceComponent" }
|
||||
}
|
||||
|
||||
/**
|
||||
* An interpolation expression component of a regex literal.
|
||||
*/
|
||||
class RegExpInterpolationComponent extends RegExpComponent, StmtSequence instanceof RegExpComponentImpl {
|
||||
class RegExpInterpolationComponent extends RegExpComponent, StmtSequence instanceof RegExpComponentImpl
|
||||
{
|
||||
private Ruby::Interpolation g;
|
||||
|
||||
RegExpInterpolationComponent() { this = TStringInterpolationComponentRegexp(g) }
|
||||
|
||||
@@ -101,7 +101,8 @@ abstract class DestructuredLhsExprImpl extends Ruby::AstNode {
|
||||
}
|
||||
|
||||
class DestructuredLeftAssignmentImpl extends DestructuredLhsExprImpl,
|
||||
Ruby::DestructuredLeftAssignment {
|
||||
Ruby::DestructuredLeftAssignment
|
||||
{
|
||||
override Ruby::AstNode getChildNode(int i) { result = this.getChild(i) }
|
||||
}
|
||||
|
||||
|
||||
@@ -326,7 +326,8 @@ private string unescapeTextComponent(string text) {
|
||||
}
|
||||
|
||||
class StringTextComponentStringOrHeredocContent extends StringTextComponentImpl,
|
||||
TStringTextComponentNonRegexpStringOrHeredocContent {
|
||||
TStringTextComponentNonRegexpStringOrHeredocContent
|
||||
{
|
||||
private Ruby::Token g;
|
||||
|
||||
StringTextComponentStringOrHeredocContent() {
|
||||
@@ -341,7 +342,8 @@ class StringTextComponentStringOrHeredocContent extends StringTextComponentImpl,
|
||||
}
|
||||
|
||||
private class StringTextComponentSimpleSymbol extends StringTextComponentImpl,
|
||||
TStringTextComponentNonRegexpSimpleSymbol {
|
||||
TStringTextComponentNonRegexpSimpleSymbol
|
||||
{
|
||||
private Ruby::SimpleSymbol g;
|
||||
|
||||
StringTextComponentSimpleSymbol() { this = TStringTextComponentNonRegexpSimpleSymbol(g) }
|
||||
@@ -355,7 +357,8 @@ private class StringTextComponentSimpleSymbol extends StringTextComponentImpl,
|
||||
}
|
||||
|
||||
private class StringTextComponentHashKeySymbol extends StringTextComponentImpl,
|
||||
TStringTextComponentNonRegexpHashKeySymbol {
|
||||
TStringTextComponentNonRegexpHashKeySymbol
|
||||
{
|
||||
private Ruby::HashKeySymbol g;
|
||||
|
||||
StringTextComponentHashKeySymbol() { this = TStringTextComponentNonRegexpHashKeySymbol(g) }
|
||||
@@ -424,7 +427,8 @@ private string unescapeEscapeSequence(string escaped) {
|
||||
* An escape sequence component of a string or string-like literal.
|
||||
*/
|
||||
class StringEscapeSequenceComponentImpl extends StringComponentImpl,
|
||||
TStringEscapeSequenceComponentNonRegexp {
|
||||
TStringEscapeSequenceComponentNonRegexp
|
||||
{
|
||||
private Ruby::EscapeSequence g;
|
||||
|
||||
StringEscapeSequenceComponentImpl() { this = TStringEscapeSequenceComponentNonRegexp(g) }
|
||||
@@ -439,7 +443,8 @@ class StringEscapeSequenceComponentImpl extends StringComponentImpl,
|
||||
}
|
||||
|
||||
class StringInterpolationComponentImpl extends StringComponentImpl,
|
||||
TStringInterpolationComponentNonRegexp {
|
||||
TStringInterpolationComponentNonRegexp
|
||||
{
|
||||
private Ruby::Interpolation g;
|
||||
|
||||
StringInterpolationComponentImpl() { this = TStringInterpolationComponentNonRegexp(g) }
|
||||
@@ -472,7 +477,8 @@ class RegExpTextComponentImpl extends RegExpComponentImpl, TStringTextComponentR
|
||||
}
|
||||
|
||||
class RegExpEscapeSequenceComponentImpl extends RegExpComponentImpl,
|
||||
TStringEscapeSequenceComponentRegexp {
|
||||
TStringEscapeSequenceComponentRegexp
|
||||
{
|
||||
private Ruby::EscapeSequence g;
|
||||
|
||||
RegExpEscapeSequenceComponentImpl() { this = TStringEscapeSequenceComponentRegexp(g) }
|
||||
@@ -488,7 +494,8 @@ class RegExpEscapeSequenceComponentImpl extends RegExpComponentImpl,
|
||||
}
|
||||
|
||||
class RegExpInterpolationComponentImpl extends RegExpComponentImpl,
|
||||
TStringInterpolationComponentRegexp {
|
||||
TStringInterpolationComponentRegexp
|
||||
{
|
||||
private Ruby::Interpolation g;
|
||||
|
||||
RegExpInterpolationComponentImpl() { this = TStringInterpolationComponentRegexp(g) }
|
||||
|
||||
@@ -607,7 +607,8 @@ private class GlobalVariableAccessReal extends GlobalVariableAccessImpl, TGlobal
|
||||
final override string toString() { result = g.getValue() }
|
||||
}
|
||||
|
||||
private class GlobalVariableAccessSynth extends GlobalVariableAccessImpl, TGlobalVariableAccessSynth {
|
||||
private class GlobalVariableAccessSynth extends GlobalVariableAccessImpl, TGlobalVariableAccessSynth
|
||||
{
|
||||
private GlobalVariable v;
|
||||
|
||||
GlobalVariableAccessSynth() { this = TGlobalVariableAccessSynth(_, _, v) }
|
||||
@@ -624,7 +625,8 @@ module InstanceVariableAccess {
|
||||
abstract class InstanceVariableAccessImpl extends VariableAccessImpl, TInstanceVariableAccess { }
|
||||
|
||||
private class InstanceVariableAccessReal extends InstanceVariableAccessImpl,
|
||||
TInstanceVariableAccessReal {
|
||||
TInstanceVariableAccessReal
|
||||
{
|
||||
private Ruby::InstanceVariable g;
|
||||
private InstanceVariable v;
|
||||
|
||||
@@ -636,7 +638,8 @@ private class InstanceVariableAccessReal extends InstanceVariableAccessImpl,
|
||||
}
|
||||
|
||||
private class InstanceVariableAccessSynth extends InstanceVariableAccessImpl,
|
||||
TInstanceVariableAccessSynth {
|
||||
TInstanceVariableAccessSynth
|
||||
{
|
||||
private InstanceVariable v;
|
||||
|
||||
InstanceVariableAccessSynth() { this = TInstanceVariableAccessSynth(_, _, v) }
|
||||
@@ -664,7 +667,8 @@ private class ClassVariableAccessReal extends ClassVariableAccessRealImpl, TClas
|
||||
}
|
||||
|
||||
private class ClassVariableAccessSynth extends ClassVariableAccessRealImpl,
|
||||
TClassVariableAccessSynth {
|
||||
TClassVariableAccessSynth
|
||||
{
|
||||
private ClassVariable v;
|
||||
|
||||
ClassVariableAccessSynth() { this = TClassVariableAccessSynth(_, _, v) }
|
||||
|
||||
@@ -243,6 +243,35 @@ module ExprNodes {
|
||||
override Literal getExpr() { result = super.getExpr() }
|
||||
}
|
||||
|
||||
private class ControlExprChildMapping extends ExprChildMapping, ControlExpr {
|
||||
override predicate relevantChild(AstNode n) { none() }
|
||||
}
|
||||
|
||||
/** A control-flow node that wraps a `ControlExpr` AST expression. */
|
||||
class ControlExprCfgNode extends ExprCfgNode {
|
||||
override string getAPrimaryQlClass() { result = "ControlExprCfgNode" }
|
||||
|
||||
override ControlExprChildMapping e;
|
||||
|
||||
override ControlExpr getExpr() { result = super.getExpr() }
|
||||
}
|
||||
|
||||
private class LhsExprChildMapping extends ExprChildMapping, LhsExpr {
|
||||
override predicate relevantChild(AstNode n) { none() }
|
||||
}
|
||||
|
||||
/** A control-flow node that wraps a `LhsExpr` AST expression. */
|
||||
class LhsExprCfgNode extends ExprCfgNode {
|
||||
override string getAPrimaryQlClass() { result = "LhsExprCfgNode" }
|
||||
|
||||
override LhsExprChildMapping e;
|
||||
|
||||
override LhsExpr getExpr() { result = super.getExpr() }
|
||||
|
||||
/** Gets a variable used in (or introduced by) this LHS. */
|
||||
Variable getAVariable() { result = e.(VariableAccess).getVariable() }
|
||||
}
|
||||
|
||||
private class AssignExprChildMapping extends ExprChildMapping, AssignExpr {
|
||||
override predicate relevantChild(AstNode n) { n = this.getAnOperand() }
|
||||
}
|
||||
@@ -256,7 +285,7 @@ module ExprNodes {
|
||||
final override AssignExpr getExpr() { result = ExprCfgNode.super.getExpr() }
|
||||
|
||||
/** Gets the LHS of this assignment. */
|
||||
final ExprCfgNode getLhs() { e.hasCfgChild(e.getLeftOperand(), this, result) }
|
||||
final LhsExprCfgNode getLhs() { e.hasCfgChild(e.getLeftOperand(), this, result) }
|
||||
|
||||
/** Gets the RHS of this assignment. */
|
||||
final ExprCfgNode getRhs() { e.hasCfgChild(e.getRightOperand(), this, result) }
|
||||
|
||||
@@ -284,7 +284,8 @@ abstract class ConditionalCompletion extends NormalCompletion {
|
||||
* A completion that represents evaluation of an expression
|
||||
* with a Boolean value.
|
||||
*/
|
||||
class BooleanCompletion extends ConditionalCompletion, NonNestedNormalCompletion, TBooleanCompletion {
|
||||
class BooleanCompletion extends ConditionalCompletion, NonNestedNormalCompletion, TBooleanCompletion
|
||||
{
|
||||
BooleanCompletion() { this = TBooleanCompletion(value) }
|
||||
|
||||
/** Gets the dual Boolean completion. */
|
||||
|
||||
@@ -465,7 +465,8 @@ module Trees {
|
||||
}
|
||||
|
||||
private class PatternVariableAccessTree extends LocalVariableAccessTree, LocalVariableWriteAccess,
|
||||
CasePattern {
|
||||
CasePattern
|
||||
{
|
||||
final override predicate last(AstNode last, Completion c) {
|
||||
super.last(last, c) and
|
||||
c.(MatchingCompletion).getValue() = true
|
||||
|
||||
@@ -92,7 +92,8 @@ class StringConstCompareBarrier extends DataFlow::Node {
|
||||
* in the `order` call.
|
||||
*/
|
||||
deprecated class StringConstCompare extends DataFlow::BarrierGuard,
|
||||
CfgNodes::ExprNodes::ComparisonOperationCfgNode {
|
||||
CfgNodes::ExprNodes::ComparisonOperationCfgNode
|
||||
{
|
||||
private CfgNode checkedNode;
|
||||
// The value of the condition that results in the node being validated.
|
||||
private boolean checkedBranch;
|
||||
@@ -160,7 +161,8 @@ class StringConstArrayInclusionCallBarrier extends DataFlow::Node {
|
||||
* in the `find_by` call.
|
||||
*/
|
||||
deprecated class StringConstArrayInclusionCall extends DataFlow::BarrierGuard,
|
||||
CfgNodes::ExprNodes::MethodCallCfgNode {
|
||||
CfgNodes::ExprNodes::MethodCallCfgNode
|
||||
{
|
||||
private CfgNode checkedNode;
|
||||
|
||||
StringConstArrayInclusionCall() { stringConstArrayInclusionCall(this, checkedNode, true) }
|
||||
|
||||
245
ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlow.qll
Normal file
245
ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlow.qll
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Provides an implementation of global (interprocedural) data flow. This file
|
||||
* re-exports the local (intraprocedural) data flow analysis from
|
||||
* `DataFlowImplSpecific::Public` and adds a global analysis, mainly exposed
|
||||
* through the `Make` and `MakeWithState` modules.
|
||||
*/
|
||||
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
import DataFlowImplCommonPublic
|
||||
private import DataFlowImpl
|
||||
|
||||
/** An input configuration for data flow. */
|
||||
signature module ConfigSig {
|
||||
/**
|
||||
* Holds if `source` is a relevant data flow source.
|
||||
*/
|
||||
predicate isSource(Node source);
|
||||
|
||||
/**
|
||||
* Holds if `sink` is a relevant data flow sink.
|
||||
*/
|
||||
predicate isSink(Node sink);
|
||||
|
||||
/**
|
||||
* Holds if data flow through `node` is prohibited. This completely removes
|
||||
* `node` from the data flow graph.
|
||||
*/
|
||||
default predicate isBarrier(Node node) { none() }
|
||||
|
||||
/** Holds if data flow into `node` is prohibited. */
|
||||
default predicate isBarrierIn(Node node) { none() }
|
||||
|
||||
/** Holds if data flow out of `node` is prohibited. */
|
||||
default predicate isBarrierOut(Node node) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
|
||||
*/
|
||||
default predicate isAdditionalFlowStep(Node node1, Node node2) { none() }
|
||||
|
||||
/**
|
||||
* Holds if an arbitrary number of implicit read steps of content `c` may be
|
||||
* taken at `node`.
|
||||
*/
|
||||
default predicate allowImplicitRead(Node node, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Gets the virtual dispatch branching limit when calculating field flow.
|
||||
* This can be overridden to a smaller value to improve performance (a
|
||||
* value of 0 disables field flow), or a larger value to get more results.
|
||||
*/
|
||||
default int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*
|
||||
* These features are generally not relevant for typical end-to-end data flow
|
||||
* queries, but should only be used for constructing paths that need to
|
||||
* somehow be pluggable in another path context.
|
||||
*/
|
||||
default FlowFeature getAFeature() { none() }
|
||||
|
||||
/** Holds if sources should be grouped in the result of `hasFlowPath`. */
|
||||
default predicate sourceGrouping(Node source, string sourceGroup) { none() }
|
||||
|
||||
/** Holds if sinks should be grouped in the result of `hasFlowPath`. */
|
||||
default predicate sinkGrouping(Node sink, string sinkGroup) { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
* This feature should only be used for debugging or when the data flow graph
|
||||
* is not visualized (as it is in a `path-problem` query).
|
||||
*/
|
||||
default predicate includeHiddenNodes() { none() }
|
||||
}
|
||||
|
||||
/** An input configuration for data flow using flow state. */
|
||||
signature module StateConfigSig {
|
||||
bindingset[this]
|
||||
class FlowState;
|
||||
|
||||
/**
|
||||
* Holds if `source` is a relevant data flow source with the given initial
|
||||
* `state`.
|
||||
*/
|
||||
predicate isSource(Node source, FlowState state);
|
||||
|
||||
/**
|
||||
* Holds if `sink` is a relevant data flow sink accepting `state`.
|
||||
*/
|
||||
predicate isSink(Node sink, FlowState state);
|
||||
|
||||
/**
|
||||
* Holds if data flow through `node` is prohibited. This completely removes
|
||||
* `node` from the data flow graph.
|
||||
*/
|
||||
default predicate isBarrier(Node node) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data flow through `node` is prohibited when the flow state is
|
||||
* `state`.
|
||||
*/
|
||||
predicate isBarrier(Node node, FlowState state);
|
||||
|
||||
/** Holds if data flow into `node` is prohibited. */
|
||||
default predicate isBarrierIn(Node node) { none() }
|
||||
|
||||
/** Holds if data flow out of `node` is prohibited. */
|
||||
default predicate isBarrierOut(Node node) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
|
||||
*/
|
||||
default predicate isAdditionalFlowStep(Node node1, Node node2) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
|
||||
* This step is only applicable in `state1` and updates the flow state to `state2`.
|
||||
*/
|
||||
predicate isAdditionalFlowStep(Node node1, FlowState state1, Node node2, FlowState state2);
|
||||
|
||||
/**
|
||||
* Holds if an arbitrary number of implicit read steps of content `c` may be
|
||||
* taken at `node`.
|
||||
*/
|
||||
default predicate allowImplicitRead(Node node, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Gets the virtual dispatch branching limit when calculating field flow.
|
||||
* This can be overridden to a smaller value to improve performance (a
|
||||
* value of 0 disables field flow), or a larger value to get more results.
|
||||
*/
|
||||
default int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*
|
||||
* These features are generally not relevant for typical end-to-end data flow
|
||||
* queries, but should only be used for constructing paths that need to
|
||||
* somehow be pluggable in another path context.
|
||||
*/
|
||||
default FlowFeature getAFeature() { none() }
|
||||
|
||||
/** Holds if sources should be grouped in the result of `hasFlowPath`. */
|
||||
default predicate sourceGrouping(Node source, string sourceGroup) { none() }
|
||||
|
||||
/** Holds if sinks should be grouped in the result of `hasFlowPath`. */
|
||||
default predicate sinkGrouping(Node sink, string sinkGroup) { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
* This feature should only be used for debugging or when the data flow graph
|
||||
* is not visualized (as it is in a `path-problem` query).
|
||||
*/
|
||||
default predicate includeHiddenNodes() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
signature int explorationLimitSig();
|
||||
|
||||
/**
|
||||
* The output of a data flow computation.
|
||||
*/
|
||||
signature module DataFlowSig {
|
||||
/**
|
||||
* A `Node` augmented with a call context (except for sinks) and an access path.
|
||||
* Only those `PathNode`s that are reachable from a source, and which can reach a sink, are generated.
|
||||
*/
|
||||
class PathNode;
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `source` to `sink`.
|
||||
*
|
||||
* The corresponding paths are generated from the end-points and the graph
|
||||
* included in the module `PathGraph`.
|
||||
*/
|
||||
predicate hasFlowPath(PathNode source, PathNode sink);
|
||||
|
||||
/**
|
||||
* Holds if data can flow from `source` to `sink`.
|
||||
*/
|
||||
predicate hasFlow(Node source, Node sink);
|
||||
|
||||
/**
|
||||
* Holds if data can flow from some source to `sink`.
|
||||
*/
|
||||
predicate hasFlowTo(Node sink);
|
||||
|
||||
/**
|
||||
* Holds if data can flow from some source to `sink`.
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a standard data flow computation.
|
||||
*/
|
||||
module Make<ConfigSig Config> implements DataFlowSig {
|
||||
private module C implements FullStateConfigSig {
|
||||
import DefaultState<Config>
|
||||
import Config
|
||||
}
|
||||
|
||||
import Impl<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a data flow computation using flow state.
|
||||
*/
|
||||
module MakeWithState<StateConfigSig Config> implements DataFlowSig {
|
||||
private module C implements FullStateConfigSig {
|
||||
import Config
|
||||
}
|
||||
|
||||
import Impl<C>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
396
ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowImpl1.qll
Normal file
396
ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowImpl1.qll
Normal file
@@ -0,0 +1,396 @@
|
||||
/**
|
||||
* DEPRECATED: Use `Make` and `MakeWithState` instead.
|
||||
*
|
||||
* Provides a `Configuration` class backwards-compatible interface to the data
|
||||
* flow library.
|
||||
*/
|
||||
|
||||
private import DataFlowImplCommon
|
||||
private import DataFlowImplSpecific::Private
|
||||
import DataFlowImplSpecific::Public
|
||||
private import DataFlowImpl
|
||||
import DataFlowImplCommonPublic
|
||||
import FlowStateString
|
||||
|
||||
/**
|
||||
* A configuration of interprocedural data flow analysis. This defines
|
||||
* sources, sinks, and any other configurable aspect of the analysis. Each
|
||||
* use of the global data flow library must define its own unique extension
|
||||
* of this abstract class. To create a configuration, extend this class with
|
||||
* a subclass whose characteristic predicate is a unique singleton string.
|
||||
* For example, write
|
||||
*
|
||||
* ```ql
|
||||
* class MyAnalysisConfiguration extends DataFlow::Configuration {
|
||||
* MyAnalysisConfiguration() { this = "MyAnalysisConfiguration" }
|
||||
* // Override `isSource` and `isSink`.
|
||||
* // Optionally override `isBarrier`.
|
||||
* // Optionally override `isAdditionalFlowStep`.
|
||||
* }
|
||||
* ```
|
||||
* Conceptually, this defines a graph where the nodes are `DataFlow::Node`s and
|
||||
* the edges are those data-flow steps that preserve the value of the node
|
||||
* along with any additional edges defined by `isAdditionalFlowStep`.
|
||||
* Specifying nodes in `isBarrier` will remove those nodes from the graph, and
|
||||
* specifying nodes in `isBarrierIn` and/or `isBarrierOut` will remove in-going
|
||||
* and/or out-going edges from those nodes, respectively.
|
||||
*
|
||||
* Then, to query whether there is flow between some `source` and `sink`,
|
||||
* write
|
||||
*
|
||||
* ```ql
|
||||
* exists(MyAnalysisConfiguration cfg | cfg.hasFlow(source, sink))
|
||||
* ```
|
||||
*
|
||||
* Multiple configurations can coexist, but two classes extending
|
||||
* `DataFlow::Configuration` should never depend on each other. One of them
|
||||
* should instead depend on a `DataFlow2::Configuration`, a
|
||||
* `DataFlow3::Configuration`, or a `DataFlow4::Configuration`.
|
||||
*/
|
||||
abstract class Configuration extends string {
|
||||
bindingset[this]
|
||||
Configuration() { any() }
|
||||
|
||||
/**
|
||||
* Holds if `source` is a relevant data flow source.
|
||||
*/
|
||||
predicate isSource(Node source) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `source` is a relevant data flow source with the given initial
|
||||
* `state`.
|
||||
*/
|
||||
predicate isSource(Node source, FlowState state) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `sink` is a relevant data flow sink.
|
||||
*/
|
||||
predicate isSink(Node sink) { none() }
|
||||
|
||||
/**
|
||||
* Holds if `sink` is a relevant data flow sink accepting `state`.
|
||||
*/
|
||||
predicate isSink(Node sink, FlowState state) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data flow through `node` is prohibited. This completely removes
|
||||
* `node` from the data flow graph.
|
||||
*/
|
||||
predicate isBarrier(Node node) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data flow through `node` is prohibited when the flow state is
|
||||
* `state`.
|
||||
*/
|
||||
predicate isBarrier(Node node, FlowState state) { none() }
|
||||
|
||||
/** Holds if data flow into `node` is prohibited. */
|
||||
predicate isBarrierIn(Node node) { none() }
|
||||
|
||||
/** Holds if data flow out of `node` is prohibited. */
|
||||
predicate isBarrierOut(Node node) { none() }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `isBarrier` and `BarrierGuard` module instead.
|
||||
*
|
||||
* Holds if data flow through nodes guarded by `guard` is prohibited.
|
||||
*/
|
||||
deprecated predicate isBarrierGuard(BarrierGuard guard) { none() }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `isBarrier` and `BarrierGuard` module instead.
|
||||
*
|
||||
* Holds if data flow through nodes guarded by `guard` is prohibited when
|
||||
* the flow state is `state`
|
||||
*/
|
||||
deprecated predicate isBarrierGuard(BarrierGuard guard, FlowState state) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
|
||||
*/
|
||||
predicate isAdditionalFlowStep(Node node1, Node node2) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `node1` to `node2` in addition to the normal data-flow steps.
|
||||
* This step is only applicable in `state1` and updates the flow state to `state2`.
|
||||
*/
|
||||
predicate isAdditionalFlowStep(Node node1, FlowState state1, Node node2, FlowState state2) {
|
||||
none()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if an arbitrary number of implicit read steps of content `c` may be
|
||||
* taken at `node`.
|
||||
*/
|
||||
predicate allowImplicitRead(Node node, ContentSet c) { none() }
|
||||
|
||||
/**
|
||||
* Gets the virtual dispatch branching limit when calculating field flow.
|
||||
* This can be overridden to a smaller value to improve performance (a
|
||||
* value of 0 disables field flow), or a larger value to get more results.
|
||||
*/
|
||||
int fieldFlowBranchLimit() { result = 2 }
|
||||
|
||||
/**
|
||||
* Gets a data flow configuration feature to add restrictions to the set of
|
||||
* valid flow paths.
|
||||
*
|
||||
* - `FeatureHasSourceCallContext`:
|
||||
* Assume that sources have some existing call context to disallow
|
||||
* conflicting return-flow directly following the source.
|
||||
* - `FeatureHasSinkCallContext`:
|
||||
* Assume that sinks have some existing call context to disallow
|
||||
* conflicting argument-to-parameter flow directly preceding the sink.
|
||||
* - `FeatureEqualSourceSinkCallContext`:
|
||||
* Implies both of the above and additionally ensures that the entire flow
|
||||
* path preserves the call context.
|
||||
*
|
||||
* These features are generally not relevant for typical end-to-end data flow
|
||||
* queries, but should only be used for constructing paths that need to
|
||||
* somehow be pluggable in another path context.
|
||||
*/
|
||||
FlowFeature getAFeature() { none() }
|
||||
|
||||
/** Holds if sources should be grouped in the result of `hasFlowPath`. */
|
||||
predicate sourceGrouping(Node source, string sourceGroup) { none() }
|
||||
|
||||
/** Holds if sinks should be grouped in the result of `hasFlowPath`. */
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) { none() }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*/
|
||||
predicate hasFlow(Node source, Node sink) { hasFlow(source, sink, this) }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from `source` to `sink` for this configuration.
|
||||
*
|
||||
* The corresponding paths are generated from the end-points and the graph
|
||||
* included in the module `PathGraph`.
|
||||
*/
|
||||
predicate hasFlowPath(PathNode source, PathNode sink) { hasFlowPath(source, sink, this) }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from some source to `sink` for this configuration.
|
||||
*/
|
||||
predicate hasFlowTo(Node sink) { hasFlowTo(sink, this) }
|
||||
|
||||
/**
|
||||
* Holds if data may flow from some source to `sink` for this configuration.
|
||||
*/
|
||||
predicate hasFlowToExpr(DataFlowExpr sink) { this.hasFlowTo(exprNode(sink)) }
|
||||
|
||||
/**
|
||||
* DEPRECATED: Use `FlowExploration<explorationLimit>` instead.
|
||||
*
|
||||
* Gets the exploration limit for `hasPartialFlow` and `hasPartialFlowRev`
|
||||
* measured in approximate number of interprocedural steps.
|
||||
*/
|
||||
deprecated int explorationLimit() { none() }
|
||||
|
||||
/**
|
||||
* Holds if hidden nodes should be included in the data flow graph.
|
||||
*
|
||||
* This feature should only be used for debugging or when the data flow graph
|
||||
* is not visualized (for example in a `path-problem` query).
|
||||
*/
|
||||
predicate includeHiddenNodes() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* This class exists to prevent mutual recursion between the user-overridden
|
||||
* member predicates of `Configuration` and the rest of the data-flow library.
|
||||
* Good performance cannot be guaranteed in the presence of such recursion, so
|
||||
* it should be replaced by using more than one copy of the data flow library.
|
||||
*/
|
||||
abstract private class ConfigurationRecursionPrevention extends Configuration {
|
||||
bindingset[this]
|
||||
ConfigurationRecursionPrevention() { any() }
|
||||
|
||||
override predicate hasFlow(Node source, Node sink) {
|
||||
strictcount(Node n | this.isSource(n)) < 0
|
||||
or
|
||||
strictcount(Node n | this.isSource(n, _)) < 0
|
||||
or
|
||||
strictcount(Node n | this.isSink(n)) < 0
|
||||
or
|
||||
strictcount(Node n | this.isSink(n, _)) < 0
|
||||
or
|
||||
strictcount(Node n1, Node n2 | this.isAdditionalFlowStep(n1, n2)) < 0
|
||||
or
|
||||
strictcount(Node n1, Node n2 | this.isAdditionalFlowStep(n1, _, n2, _)) < 0
|
||||
or
|
||||
super.hasFlow(source, sink)
|
||||
}
|
||||
}
|
||||
|
||||
/** A bridge class to access the deprecated `isBarrierGuard`. */
|
||||
private class BarrierGuardGuardedNodeBridge extends Unit {
|
||||
abstract predicate guardedNode(Node n, Configuration config);
|
||||
|
||||
abstract predicate guardedNode(Node n, FlowState state, Configuration config);
|
||||
}
|
||||
|
||||
private class BarrierGuardGuardedNode extends BarrierGuardGuardedNodeBridge {
|
||||
deprecated override predicate guardedNode(Node n, Configuration config) {
|
||||
exists(BarrierGuard g |
|
||||
config.isBarrierGuard(g) and
|
||||
n = g.getAGuardedNode()
|
||||
)
|
||||
}
|
||||
|
||||
deprecated override predicate guardedNode(Node n, FlowState state, Configuration config) {
|
||||
exists(BarrierGuard g |
|
||||
config.isBarrierGuard(g, state) and
|
||||
n = g.getAGuardedNode()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private FlowState relevantState(Configuration config) {
|
||||
config.isSource(_, result) or
|
||||
config.isSink(_, result) or
|
||||
config.isBarrier(_, result) or
|
||||
config.isAdditionalFlowStep(_, result, _, _) or
|
||||
config.isAdditionalFlowStep(_, _, _, result)
|
||||
}
|
||||
|
||||
private newtype TConfigState =
|
||||
TMkConfigState(Configuration config, FlowState state) {
|
||||
state = relevantState(config) or state instanceof FlowStateEmpty
|
||||
}
|
||||
|
||||
private Configuration getConfig(TConfigState state) { state = TMkConfigState(result, _) }
|
||||
|
||||
private FlowState getState(TConfigState state) { state = TMkConfigState(_, result) }
|
||||
|
||||
private predicate singleConfiguration() { 1 = strictcount(Configuration c) }
|
||||
|
||||
private module Config implements FullStateConfigSig {
|
||||
class FlowState = TConfigState;
|
||||
|
||||
predicate isSource(Node source, FlowState state) {
|
||||
getConfig(state).isSource(source, getState(state))
|
||||
or
|
||||
getConfig(state).isSource(source) and getState(state) instanceof FlowStateEmpty
|
||||
}
|
||||
|
||||
predicate isSink(Node sink, FlowState state) {
|
||||
getConfig(state).isSink(sink, getState(state))
|
||||
or
|
||||
getConfig(state).isSink(sink) and getState(state) instanceof FlowStateEmpty
|
||||
}
|
||||
|
||||
predicate isBarrier(Node node) { none() }
|
||||
|
||||
predicate isBarrier(Node node, FlowState state) {
|
||||
getConfig(state).isBarrier(node, getState(state)) or
|
||||
getConfig(state).isBarrier(node) or
|
||||
any(BarrierGuardGuardedNodeBridge b).guardedNode(node, getState(state), getConfig(state)) or
|
||||
any(BarrierGuardGuardedNodeBridge b).guardedNode(node, getConfig(state))
|
||||
}
|
||||
|
||||
predicate isBarrierIn(Node node) { any(Configuration config).isBarrierIn(node) }
|
||||
|
||||
predicate isBarrierOut(Node node) { any(Configuration config).isBarrierOut(node) }
|
||||
|
||||
predicate isAdditionalFlowStep(Node node1, Node node2) {
|
||||
singleConfiguration() and
|
||||
any(Configuration config).isAdditionalFlowStep(node1, node2)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(Node node1, FlowState state1, Node node2, FlowState state2) {
|
||||
getConfig(state1).isAdditionalFlowStep(node1, getState(state1), node2, getState(state2)) and
|
||||
getConfig(state2) = getConfig(state1)
|
||||
or
|
||||
not singleConfiguration() and
|
||||
getConfig(state1).isAdditionalFlowStep(node1, node2) and
|
||||
state2 = state1
|
||||
}
|
||||
|
||||
predicate allowImplicitRead(Node node, ContentSet c) {
|
||||
any(Configuration config).allowImplicitRead(node, c)
|
||||
}
|
||||
|
||||
int fieldFlowBranchLimit() { result = min(any(Configuration config).fieldFlowBranchLimit()) }
|
||||
|
||||
FlowFeature getAFeature() { result = any(Configuration config).getAFeature() }
|
||||
|
||||
predicate sourceGrouping(Node source, string sourceGroup) {
|
||||
any(Configuration config).sourceGrouping(source, sourceGroup)
|
||||
}
|
||||
|
||||
predicate sinkGrouping(Node sink, string sinkGroup) {
|
||||
any(Configuration config).sinkGrouping(sink, sinkGroup)
|
||||
}
|
||||
|
||||
predicate includeHiddenNodes() { any(Configuration config).includeHiddenNodes() }
|
||||
}
|
||||
|
||||
private import Impl<Config> as I
|
||||
import I
|
||||
|
||||
/**
|
||||
* A `Node` augmented with a call context (except for sinks), an access path, and a configuration.
|
||||
* Only those `PathNode`s that are reachable from a source, and which can reach a sink, are generated.
|
||||
*/
|
||||
class PathNode instanceof I::PathNode {
|
||||
/** Gets a textual representation of this element. */
|
||||
final string toString() { result = super.toString() }
|
||||
|
||||
/**
|
||||
* Gets a textual representation of this element, including a textual
|
||||
* representation of the call context.
|
||||
*/
|
||||
final string toStringWithContext() { result = super.toStringWithContext() }
|
||||
|
||||
/**
|
||||
* Holds if this element is at the specified location.
|
||||
* The location spans column `startcolumn` of line `startline` to
|
||||
* column `endcolumn` of line `endline` in file `filepath`.
|
||||
* For more information, see
|
||||
* [Locations](https://codeql.github.com/docs/writing-codeql-queries/providing-locations-in-codeql-queries/).
|
||||
*/
|
||||
final predicate hasLocationInfo(
|
||||
string filepath, int startline, int startcolumn, int endline, int endcolumn
|
||||
) {
|
||||
super.hasLocationInfo(filepath, startline, startcolumn, endline, endcolumn)
|
||||
}
|
||||
|
||||
/** Gets the underlying `Node`. */
|
||||
final Node getNode() { result = super.getNode() }
|
||||
|
||||
/** Gets the `FlowState` of this node. */
|
||||
final FlowState getState() { result = getState(super.getState()) }
|
||||
|
||||
/** Gets the associated configuration. */
|
||||
final Configuration getConfiguration() { result = getConfig(super.getState()) }
|
||||
|
||||
/** Gets a successor of this node, if any. */
|
||||
final PathNode getASuccessor() { result = super.getASuccessor() }
|
||||
|
||||
/** Holds if this node is a source. */
|
||||
final predicate isSource() { super.isSource() }
|
||||
|
||||
/** Holds if this node is a grouping of source nodes. */
|
||||
final predicate isSourceGroup(string group) { super.isSourceGroup(group) }
|
||||
|
||||
/** Holds if this node is a grouping of sink nodes. */
|
||||
final predicate isSinkGroup(string group) { super.isSinkGroup(group) }
|
||||
}
|
||||
|
||||
private predicate hasFlow(Node source, Node sink, Configuration config) {
|
||||
exists(PathNode source0, PathNode sink0 |
|
||||
hasFlowPath(source0, sink0, config) and
|
||||
source0.getNode() = source and
|
||||
sink0.getNode() = sink
|
||||
)
|
||||
}
|
||||
|
||||
private predicate hasFlowPath(PathNode source, PathNode sink, Configuration config) {
|
||||
hasFlowPath(source, sink) and source.getConfiguration() = config
|
||||
}
|
||||
|
||||
private predicate hasFlowTo(Node sink, Configuration config) { hasFlow(_, sink, config) }
|
||||
|
||||
predicate flowsTo = hasFlow/3;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,18 @@ private import DataFlowImplSpecific::Public
|
||||
import Cached
|
||||
|
||||
module DataFlowImplCommonPublic {
|
||||
/** A state value to track during data flow. */
|
||||
class FlowState = string;
|
||||
/** Provides `FlowState = string`. */
|
||||
module FlowStateString {
|
||||
/** A state value to track during data flow. */
|
||||
class FlowState = string;
|
||||
|
||||
/**
|
||||
* The default state, which is used when the state is unspecified for a source
|
||||
* or a sink.
|
||||
*/
|
||||
class FlowStateEmpty extends FlowState {
|
||||
FlowStateEmpty() { this = "" }
|
||||
/**
|
||||
* The default state, which is used when the state is unspecified for a source
|
||||
* or a sink.
|
||||
*/
|
||||
class FlowStateEmpty extends FlowState {
|
||||
FlowStateEmpty() { this = "" }
|
||||
}
|
||||
}
|
||||
|
||||
private newtype TFlowFeature =
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1498,3 +1498,12 @@ class AdditionalJumpStep extends Unit {
|
||||
*/
|
||||
abstract predicate step(Node pred, Node succ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an additional term that is added to the `join` and `branch` computations to reflect
|
||||
* an additional forward or backwards branching factor that is not taken into account
|
||||
* when calculating the (virtual) dispatch cost.
|
||||
*
|
||||
* Argument `arg` is part of a path from a source to a sink, and `p` is the target parameter.
|
||||
*/
|
||||
int getAdditionalFlowIntoCallNodeTerm(ArgumentNode arg, ParameterNode p) { none() }
|
||||
|
||||
@@ -984,10 +984,109 @@ class ClassNode extends ModuleNode {
|
||||
ClassNode() { this.isClass() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a literal expression.
|
||||
*/
|
||||
class LiteralNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::LiteralCfgNode literalCfgNode;
|
||||
|
||||
LiteralNode() { this.asExpr() = literalCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `Literal`. */
|
||||
Literal asLiteralAstNode() { result = literalCfgNode.getExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to an operation expression.
|
||||
*/
|
||||
class OperationNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::OperationCfgNode operationCfgNode;
|
||||
|
||||
OperationNode() { this.asExpr() = operationCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as an `Operation`. */
|
||||
Operation asOperationAstNode() { result = operationCfgNode.getExpr() }
|
||||
|
||||
/** Gets the operator of this operation. */
|
||||
final string getOperator() { result = operationCfgNode.getOperator() }
|
||||
|
||||
/** Gets an operand of this operation. */
|
||||
final Node getAnOperand() { result.asExpr() = operationCfgNode.getAnOperand() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a control expression (e.g. `if`, `while`, `for`).
|
||||
*/
|
||||
class ControlExprNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::ControlExprCfgNode controlExprCfgNode;
|
||||
|
||||
ControlExprNode() { this.asExpr() = controlExprCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `ControlExpr`. */
|
||||
ControlExpr asControlExprAstNode() { result = controlExprCfgNode.getExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a variable access expression.
|
||||
*/
|
||||
class VariableAccessNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::VariableAccessCfgNode variableAccessCfgNode;
|
||||
|
||||
VariableAccessNode() { this.asExpr() = variableAccessCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `VariableAccess`. */
|
||||
VariableAccess asVariableAccessAstNode() { result = variableAccessCfgNode.getExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a constant access expression.
|
||||
*/
|
||||
class ConstantAccessNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::ConstantAccessCfgNode constantAccessCfgNode;
|
||||
|
||||
ConstantAccessNode() { this.asExpr() = constantAccessCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `ConstantAccess`. */
|
||||
ConstantAccess asConstantAccessAstNode() { result = constantAccessCfgNode.getExpr() }
|
||||
|
||||
/** Gets the node corresponding to the scope expression. */
|
||||
final Node getScopeNode() { result.asExpr() = constantAccessCfgNode.getScopeExpr() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a LHS expression.
|
||||
*/
|
||||
class LhsExprNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::LhsExprCfgNode lhsExprCfgNode;
|
||||
|
||||
LhsExprNode() { this.asExpr() = lhsExprCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `LhsExpr`. */
|
||||
LhsExpr asLhsExprAstNode() { result = lhsExprCfgNode.getExpr() }
|
||||
|
||||
/** Gets a variable used in (or introduced by) this LHS. */
|
||||
Variable getAVariable() { result = lhsExprCfgNode.getAVariable() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a statement sequence expression.
|
||||
*/
|
||||
class StmtSequenceNode extends ExprNode {
|
||||
private CfgNodes::ExprNodes::StmtSequenceCfgNode stmtSequenceCfgNode;
|
||||
|
||||
StmtSequenceNode() { this.asExpr() = stmtSequenceCfgNode }
|
||||
|
||||
/** Gets the underlying AST node as a `StmtSequence`. */
|
||||
StmtSequence asStmtSequenceAstNode() { result = stmtSequenceCfgNode.getExpr() }
|
||||
|
||||
/** Gets the last statement in this sequence, if any. */
|
||||
final ExprNode getLastStmt() { result.asExpr() = stmtSequenceCfgNode.getLastStmt() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A data flow node corresponding to a method, block, or lambda expression.
|
||||
*/
|
||||
class CallableNode extends ExprNode {
|
||||
class CallableNode extends StmtSequenceNode {
|
||||
private Callable callable;
|
||||
|
||||
CallableNode() { this.asExpr().getExpr() = callable }
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Provides classes for performing local (intra-procedural) and
|
||||
* global (inter-procedural) taint-tracking analyses.
|
||||
*/
|
||||
|
||||
import TaintTrackingParameter::Public
|
||||
private import TaintTrackingParameter::Private
|
||||
|
||||
private module AddTaintDefaults<DataFlowInternal::FullStateConfigSig Config> implements
|
||||
DataFlowInternal::FullStateConfigSig
|
||||
{
|
||||
import Config
|
||||
|
||||
predicate isBarrier(DataFlow::Node node) {
|
||||
Config::isBarrier(node) or defaultTaintSanitizer(node)
|
||||
}
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
Config::isAdditionalFlowStep(node1, node2) or
|
||||
defaultAdditionalTaintStep(node1, node2)
|
||||
}
|
||||
|
||||
predicate allowImplicitRead(DataFlow::Node node, DataFlow::ContentSet c) {
|
||||
Config::allowImplicitRead(node, c)
|
||||
or
|
||||
(
|
||||
Config::isSink(node, _) or
|
||||
Config::isAdditionalFlowStep(node, _) or
|
||||
Config::isAdditionalFlowStep(node, _, _, _)
|
||||
) and
|
||||
defaultImplicitTaintRead(node, c)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a standard taint tracking computation.
|
||||
*/
|
||||
module Make<DataFlow::ConfigSig Config> implements DataFlow::DataFlowSig {
|
||||
private module Config0 implements DataFlowInternal::FullStateConfigSig {
|
||||
import DataFlowInternal::DefaultState<Config>
|
||||
import Config
|
||||
}
|
||||
|
||||
private module C implements DataFlowInternal::FullStateConfigSig {
|
||||
import AddTaintDefaults<Config0>
|
||||
}
|
||||
|
||||
import DataFlowInternal::Impl<C>
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a taint tracking computation using flow state.
|
||||
*/
|
||||
module MakeWithState<DataFlow::StateConfigSig Config> implements DataFlow::DataFlowSig {
|
||||
private module Config0 implements DataFlowInternal::FullStateConfigSig {
|
||||
import Config
|
||||
}
|
||||
|
||||
private module C implements DataFlowInternal::FullStateConfigSig {
|
||||
import AddTaintDefaults<Config0>
|
||||
}
|
||||
|
||||
import DataFlowInternal::Impl<C>
|
||||
}
|
||||
@@ -2,5 +2,6 @@ import codeql.ruby.dataflow.internal.TaintTrackingPublic as Public
|
||||
|
||||
module Private {
|
||||
import codeql.ruby.DataFlow::DataFlow as DataFlow
|
||||
import codeql.ruby.dataflow.internal.DataFlowImpl as DataFlowInternal
|
||||
import codeql.ruby.dataflow.internal.TaintTrackingPrivate
|
||||
}
|
||||
|
||||
137
ruby/ql/lib/codeql/ruby/experimental/ZipSlipCustomizations.qll
Normal file
137
ruby/ql/lib/codeql/ruby/experimental/ZipSlipCustomizations.qll
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for reasoning about
|
||||
* zip slip vulnerabilities, as well as extension points for
|
||||
* adding your own.
|
||||
*/
|
||||
|
||||
private import codeql.ruby.AST
|
||||
private import codeql.ruby.ApiGraphs
|
||||
private import codeql.ruby.CFG
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.dataflow.BarrierGuards
|
||||
private import codeql.ruby.dataflow.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks and sanitizers for reasoning about
|
||||
* zip slip vulnerabilities, as well as extension points for
|
||||
* adding your own.
|
||||
*/
|
||||
module ZipSlip {
|
||||
/**
|
||||
* A data flow source for zip slip vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for zip slip vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A sanitizer for zip slip vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A file system access, considered as a flow sink.
|
||||
*/
|
||||
class FileSystemAccessAsSink extends Sink {
|
||||
FileSystemAccessAsSink() { this = any(FileSystemAccess e).getAPathArgument() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Zlib::GzipReader.open(path)`, considered a flow source.
|
||||
*/
|
||||
private class GzipReaderOpen extends Source {
|
||||
GzipReaderOpen() {
|
||||
(
|
||||
this = API::getTopLevelMember("Zlib").getMember("GzipReader").getReturn("open").asSource()
|
||||
or
|
||||
this = API::getTopLevelMember("Zlib").getMember("GzipReader").getInstance().asSource()
|
||||
) and
|
||||
// If argument refers to a string object, then it's a hardcoded path and
|
||||
// this file is safe.
|
||||
not this.(DataFlow::CallNode)
|
||||
.getArgument(0)
|
||||
.getALocalSource()
|
||||
.getConstantValue()
|
||||
.isStringlikeValue(_)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Gem::Package::TarReader.new(file_stream)`, considered a flow source.
|
||||
*/
|
||||
private class TarReaderInstance extends Source {
|
||||
TarReaderInstance() {
|
||||
exists(API::MethodAccessNode newTarReader |
|
||||
newTarReader =
|
||||
API::getTopLevelMember("Gem").getMember("Package").getMember("TarReader").getMethod("new")
|
||||
|
|
||||
// Unlike in two other modules, there's no check for the constant path because TarReader class is called with an `io` object and not filepath.
|
||||
// This, of course, can be modeled but probably in the internal IO.qll file
|
||||
// For now, I'm leaving this prone to false-positives
|
||||
not exists(newTarReader.getBlock()) and this = newTarReader.getReturn().asSource()
|
||||
or
|
||||
this = newTarReader.getBlock().getParameter(0).asSource()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `Zip::File.open(path)`, considered a flow source.
|
||||
*/
|
||||
private class ZipFileOpen extends Source {
|
||||
ZipFileOpen() {
|
||||
exists(API::MethodAccessNode zipOpen |
|
||||
zipOpen = API::getTopLevelMember("Zip").getMember("File").getMethod("open") and
|
||||
// If argument refers to a string object, then it's a hardcoded path and
|
||||
// this file is safe.
|
||||
not zipOpen
|
||||
.getCallNode()
|
||||
.getArgument(0)
|
||||
.getALocalSource()
|
||||
.getConstantValue()
|
||||
.isStringlikeValue(_)
|
||||
|
|
||||
// the case with variable assignment `zip_file = Zip::File.open(path)`
|
||||
not exists(zipOpen.getBlock()) and this = zipOpen.getReturn().asSource()
|
||||
or
|
||||
// the case with direct block`Zip::File.open(path) do |zip_file|` case
|
||||
this = zipOpen.getBlock().getParameter(0).asSource()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparison with a constant string, considered as a sanitizer-guard.
|
||||
*/
|
||||
private class StringConstCompareAsSanitizer extends Sanitizer, StringConstCompareBarrier { }
|
||||
|
||||
/**
|
||||
* An inclusion check against an array of constant strings, considered as a
|
||||
* sanitizer-guard.
|
||||
*/
|
||||
private class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
|
||||
/**
|
||||
* A sanitizer like `File.expand_path(path).start_with?` where `path` is a path of a single entry inside the archive.
|
||||
* It is assumed that if `File.expand_path` is called, it is to verify the path is safe so there's no modeling of `start_with?` or other comparisons to avoid false-negatives.
|
||||
*/
|
||||
private class ExpandedPathStartsWithAsSanitizer extends Sanitizer {
|
||||
ExpandedPathStartsWithAsSanitizer() {
|
||||
exists(DataFlow::CallNode cn |
|
||||
cn.getMethodName() = "expand_path" and
|
||||
this = cn.getArgument(0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Existing PathSanitization model created for regular path traversals
|
||||
*/
|
||||
private class PathSanitizationAsSanitizer extends Sanitizer instanceof Path::PathSanitization { }
|
||||
}
|
||||
38
ruby/ql/lib/codeql/ruby/experimental/ZipSlipQuery.qll
Normal file
38
ruby/ql/lib/codeql/ruby/experimental/ZipSlipQuery.qll
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Provides a taint tracking configuration for reasoning about
|
||||
* zip slip vulnerabilities.
|
||||
*/
|
||||
|
||||
import ZipSlipCustomizations
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.TaintTracking
|
||||
private import codeql.ruby.ApiGraphs
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about zip slip
|
||||
* vulnerabilities.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "ZipSlip" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof ZipSlip::Source }
|
||||
|
||||
override predicate isSink(DataFlow::Node sink) { sink instanceof ZipSlip::Sink }
|
||||
|
||||
/**
|
||||
* This should actually be
|
||||
* `and cn = API::getTopLevelMember("Gem").getMember("Package").getMember("TarReader").getMember("Entry").getAMethodCall("full_name")` and similar for other classes
|
||||
* but I couldn't make it work so there's only check for the method name called on the entry. It is `full_name` for `Gem::Package::TarReader::Entry` and `Zlib`
|
||||
* and `name` for `Zip::File`
|
||||
*/
|
||||
override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
exists(DataFlow::CallNode cn |
|
||||
cn.getReceiver() = nodeFrom and
|
||||
cn.getMethodName() in ["full_name", "name"] and
|
||||
cn = nodeTo
|
||||
)
|
||||
}
|
||||
|
||||
override predicate isSanitizer(DataFlow::Node node) { node instanceof ZipSlip::Sanitizer }
|
||||
}
|
||||
@@ -224,7 +224,8 @@ private module Request {
|
||||
}
|
||||
|
||||
abstract private class RequestInputAccess extends RequestMethodCall,
|
||||
Http::Server::RequestInputAccess::Range {
|
||||
Http::Server::RequestInputAccess::Range
|
||||
{
|
||||
override string getSourceType() { result = "ActionDispatch::Request#" + this.getMethodName() }
|
||||
}
|
||||
|
||||
@@ -520,15 +521,15 @@ ActionControllerClass getAssociatedControllerClass(ErbFile f) {
|
||||
* templates in `app/views/` and `app/views/layouts/`.
|
||||
*/
|
||||
predicate controllerTemplateFile(ActionControllerClass cls, ErbFile templateFile) {
|
||||
exists(string templatesPath, string sourcePrefix, string subPath, string controllerPath |
|
||||
exists(string sourcePrefix, string subPath, string controllerPath |
|
||||
controllerPath = cls.getLocation().getFile().getRelativePath() and
|
||||
templatesPath = templateFile.getParentContainer().getRelativePath() and
|
||||
// `sourcePrefix` is either a prefix path ending in a slash, or empty if
|
||||
// the rails app is at the source root
|
||||
sourcePrefix = [controllerPath.regexpCapture("^(.*/)app/controllers/(?:.*?)/(?:[^/]*)$", 1), ""] and
|
||||
controllerPath = sourcePrefix + "app/controllers/" + subPath + "_controller.rb" and
|
||||
(
|
||||
templatesPath = sourcePrefix + "app/views/" + subPath or
|
||||
sourcePrefix + "app/views/" + subPath = templateFile.getParentContainer().getRelativePath()
|
||||
or
|
||||
templateFile.getRelativePath().matches(sourcePrefix + "app/views/layouts/" + subPath + "%")
|
||||
)
|
||||
)
|
||||
@@ -556,7 +557,8 @@ class ActionControllerSkipForgeryProtectionCall extends CsrfProtectionSetting::R
|
||||
* A call to `protect_from_forgery`.
|
||||
*/
|
||||
private class ActionControllerProtectFromForgeryCall extends CsrfProtectionSetting::Range,
|
||||
DataFlow::CallNode {
|
||||
DataFlow::CallNode
|
||||
{
|
||||
ActionControllerProtectFromForgeryCall() {
|
||||
this = actionControllerInstance().getAMethodCall("protect_from_forgery")
|
||||
}
|
||||
@@ -576,7 +578,8 @@ private class ActionControllerProtectFromForgeryCall extends CsrfProtectionSetti
|
||||
* A call to `send_file`, which sends the file at the given path to the client.
|
||||
*/
|
||||
private class SendFile extends FileSystemAccess::Range, Http::Server::HttpResponse::Range,
|
||||
DataFlow::CallNode {
|
||||
DataFlow::CallNode
|
||||
{
|
||||
SendFile() {
|
||||
this = [actionControllerInstance(), Response::response()].getAMethodCall("send_file")
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ private class ActionViewCookiesCall extends ActionViewContextCall, CookiesCallIm
|
||||
* A call to `render`, `render_to_body` or `render_to_string`, seen as an
|
||||
* `HttpResponse`.
|
||||
*/
|
||||
private class RenderCallAsHttpResponse extends DataFlow::CallNode, Http::Server::HttpResponse::Range {
|
||||
private class RenderCallAsHttpResponse extends DataFlow::CallNode, Http::Server::HttpResponse::Range
|
||||
{
|
||||
RenderCallAsHttpResponse() {
|
||||
this.asExpr().getExpr() instanceof Rails::RenderCall or
|
||||
this.asExpr().getExpr() instanceof Rails::RenderToCall
|
||||
|
||||
@@ -219,7 +219,8 @@ class ActiveRecordSqlExecutionRange extends SqlExecution::Range {
|
||||
* A node that may evaluate to one or more `ActiveRecordModelClass` instances.
|
||||
*/
|
||||
abstract class ActiveRecordModelInstantiation extends OrmInstantiation::Range,
|
||||
DataFlow::LocalSourceNode {
|
||||
DataFlow::LocalSourceNode
|
||||
{
|
||||
/**
|
||||
* Gets the `ActiveRecordModelClass` that this instance belongs to.
|
||||
*/
|
||||
@@ -272,7 +273,8 @@ private Expr getUltimateReceiver(MethodCall call) {
|
||||
}
|
||||
|
||||
// A call to `find`, `where`, etc. that may return active record model object(s)
|
||||
private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation, DataFlow::CallNode {
|
||||
private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation, DataFlow::CallNode
|
||||
{
|
||||
private ActiveRecordModelClass cls;
|
||||
|
||||
ActiveRecordModelFinderCall() {
|
||||
@@ -305,7 +307,8 @@ private class ActiveRecordModelFinderCall extends ActiveRecordModelInstantiation
|
||||
|
||||
// A `self` reference that may resolve to an active record model object
|
||||
private class ActiveRecordModelClassSelfReference extends ActiveRecordModelInstantiation,
|
||||
SsaSelfDefinitionNode {
|
||||
SsaSelfDefinitionNode
|
||||
{
|
||||
private ActiveRecordModelClass cls;
|
||||
|
||||
ActiveRecordModelClassSelfReference() {
|
||||
@@ -465,7 +468,8 @@ private module Persistence {
|
||||
|
||||
/** A call to e.g. `user.update(name: "foo")` */
|
||||
private class UpdateLikeInstanceMethodCall extends PersistentWriteAccess::Range,
|
||||
ActiveRecordInstanceMethodCall {
|
||||
ActiveRecordInstanceMethodCall
|
||||
{
|
||||
UpdateLikeInstanceMethodCall() {
|
||||
this.getMethodName() = ["update", "update!", "update_attributes", "update_attributes!"]
|
||||
}
|
||||
@@ -485,7 +489,8 @@ private module Persistence {
|
||||
|
||||
/** A call to e.g. `user.update_attribute(name, "foo")` */
|
||||
private class UpdateAttributeCall extends PersistentWriteAccess::Range,
|
||||
ActiveRecordInstanceMethodCall {
|
||||
ActiveRecordInstanceMethodCall
|
||||
{
|
||||
UpdateAttributeCall() { this.getMethodName() = "update_attribute" }
|
||||
|
||||
override DataFlow::Node getValue() {
|
||||
@@ -688,7 +693,8 @@ private class ActiveRecordCollectionProxyMethodCall extends DataFlow::CallNode {
|
||||
/**
|
||||
* A call to an association method which yields ActiveRecord instances.
|
||||
*/
|
||||
private class ActiveRecordAssociationModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordAssociationMethodCall {
|
||||
private class ActiveRecordAssociationModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordAssociationMethodCall
|
||||
{
|
||||
override ActiveRecordModelClass getClass() {
|
||||
result = this.(ActiveRecordAssociationMethodCall).getAssociation().getTargetClass()
|
||||
}
|
||||
@@ -697,7 +703,8 @@ private class ActiveRecordAssociationModelInstantiation extends ActiveRecordMode
|
||||
/**
|
||||
* A call to a method on a collection proxy which yields ActiveRecord instances.
|
||||
*/
|
||||
private class ActiveRecordCollectionProxyModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordCollectionProxyMethodCall {
|
||||
private class ActiveRecordCollectionProxyModelInstantiation extends ActiveRecordModelInstantiation instanceof ActiveRecordCollectionProxyMethodCall
|
||||
{
|
||||
override ActiveRecordModelClass getClass() {
|
||||
result = this.(ActiveRecordCollectionProxyMethodCall).getAssociation().getTargetClass()
|
||||
}
|
||||
|
||||
@@ -215,7 +215,8 @@ module ActiveResource {
|
||||
}
|
||||
|
||||
private class ModelClassMethodCallAsHttpRequest extends Http::Client::Request::Range,
|
||||
ModelClassMethodCall {
|
||||
ModelClassMethodCall
|
||||
{
|
||||
ModelClass cls;
|
||||
|
||||
ModelClassMethodCallAsHttpRequest() {
|
||||
@@ -239,7 +240,8 @@ module ActiveResource {
|
||||
}
|
||||
|
||||
private class ModelInstanceMethodCallAsHttpRequest extends Http::Client::Request::Range,
|
||||
ModelInstanceMethodCall {
|
||||
ModelInstanceMethodCall
|
||||
{
|
||||
ModelClass cls;
|
||||
|
||||
ModelInstanceMethodCallAsHttpRequest() {
|
||||
|
||||
@@ -166,7 +166,8 @@ module ActiveStorage {
|
||||
* A call on an ActiveStorage object that results in an image transformation.
|
||||
* Arguments to these calls may be executed as system commands.
|
||||
*/
|
||||
private class ImageProcessingCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
|
||||
private class ImageProcessingCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
ImageProcessingCall() {
|
||||
this.getReceiver() instanceof BlobInstance and
|
||||
this.getMethodName() = ["variant", "preview", "representation"]
|
||||
|
||||
@@ -81,7 +81,8 @@ module File {
|
||||
}
|
||||
|
||||
private class FileModulePermissionModification extends FileSystemPermissionModification::Range,
|
||||
DataFlow::CallNode {
|
||||
DataFlow::CallNode
|
||||
{
|
||||
private DataFlow::Node permissionArg;
|
||||
|
||||
FileModulePermissionModification() {
|
||||
@@ -164,7 +165,8 @@ module FileUtils {
|
||||
}
|
||||
|
||||
private class FileUtilsPermissionModification extends FileSystemPermissionModification::Range,
|
||||
DataFlow::CallNode {
|
||||
DataFlow::CallNode
|
||||
{
|
||||
private DataFlow::Node permissionArg;
|
||||
|
||||
FileUtilsPermissionModification() {
|
||||
|
||||
@@ -227,7 +227,8 @@ private module Settings {
|
||||
* production code.
|
||||
*/
|
||||
private class AllowForgeryProtectionSetting extends Settings::BooleanSetting,
|
||||
CsrfProtectionSetting::Range {
|
||||
CsrfProtectionSetting::Range
|
||||
{
|
||||
AllowForgeryProtectionSetting() {
|
||||
this = Config::actionController().getAMethodCall("allow_forgery_protection=")
|
||||
}
|
||||
@@ -241,7 +242,8 @@ private class AllowForgeryProtectionSetting extends Settings::BooleanSetting,
|
||||
* https://ruby-doc.org/stdlib-2.7.1/libdoc/openssl/rdoc/OpenSSL/Cipher.html
|
||||
*/
|
||||
private class EncryptedCookieCipherSetting extends Settings::StringlikeSetting,
|
||||
CookieSecurityConfigurationSetting::Range {
|
||||
CookieSecurityConfigurationSetting::Range
|
||||
{
|
||||
EncryptedCookieCipherSetting() {
|
||||
this = Config::actionDispatch().getAMethodCall("encrypted_cookie_cipher=")
|
||||
}
|
||||
@@ -261,7 +263,8 @@ private class EncryptedCookieCipherSetting extends Settings::StringlikeSetting,
|
||||
* than the older AES-256-CBC cipher. Defaults to true.
|
||||
*/
|
||||
private class UseAuthenticatedCookieEncryptionSetting extends Settings::BooleanSetting,
|
||||
CookieSecurityConfigurationSetting::Range {
|
||||
CookieSecurityConfigurationSetting::Range
|
||||
{
|
||||
UseAuthenticatedCookieEncryptionSetting() {
|
||||
this = Config::actionDispatch().getAMethodCall("use_authenticated_cookie_encryption=")
|
||||
}
|
||||
@@ -283,7 +286,8 @@ private class UseAuthenticatedCookieEncryptionSetting extends Settings::BooleanS
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#strict
|
||||
*/
|
||||
private class CookiesSameSiteProtectionSetting extends Settings::NillableStringlikeSetting,
|
||||
CookieSecurityConfigurationSetting::Range {
|
||||
CookieSecurityConfigurationSetting::Range
|
||||
{
|
||||
CookiesSameSiteProtectionSetting() {
|
||||
this = Config::actionDispatch().getAMethodCall("cookies_same_site_protection=")
|
||||
}
|
||||
|
||||
@@ -27,7 +27,8 @@ module Railties {
|
||||
* A call to `Rails::Generators::Actions#execute_command`.
|
||||
* This method concatenates its first and second arguments and executes the result as a shell command.
|
||||
*/
|
||||
private class ExecuteCommandCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
|
||||
private class ExecuteCommandCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
ExecuteCommandCall() {
|
||||
this = generatorsActionsClass().getAnInstanceSelf().getAMethodCall("execute_command")
|
||||
}
|
||||
@@ -40,7 +41,8 @@ module Railties {
|
||||
/**
|
||||
* A call to a method in `Rails::Generators::Actions` which delegates to `execute_command`.
|
||||
*/
|
||||
private class ExecuteCommandWrapperCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode {
|
||||
private class ExecuteCommandWrapperCall extends SystemCommandExecution::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
ExecuteCommandWrapperCall() {
|
||||
this =
|
||||
generatorsActionsClass()
|
||||
|
||||
@@ -73,7 +73,8 @@ module Twirp {
|
||||
|
||||
/** A parameter that will receive parts of the url when handling an incoming request. */
|
||||
class UnmarshaledParameter extends Http::Server::RequestInputAccess::Range,
|
||||
DataFlow::ParameterNode {
|
||||
DataFlow::ParameterNode
|
||||
{
|
||||
UnmarshaledParameter() {
|
||||
exists(ServiceInstantiation i | i.getAHandlerMethod().getParameter(0) = this.asParameter())
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ private API::Node digest(Cryptography::HashingAlgorithm algo) {
|
||||
}
|
||||
|
||||
/** A call that hashes some input using a hashing algorithm from the `Digest` module. */
|
||||
private class DigestCall extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode {
|
||||
private class DigestCall extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
Cryptography::HashingAlgorithm algo;
|
||||
|
||||
DigestCall() {
|
||||
|
||||
@@ -114,7 +114,8 @@ module String {
|
||||
}
|
||||
|
||||
abstract private class SimpleSummarizedCallable extends SummarizedCallable,
|
||||
FlowSummary::SimpleSummarizedCallable {
|
||||
FlowSummary::SimpleSummarizedCallable
|
||||
{
|
||||
bindingset[this]
|
||||
SimpleSummarizedCallable() { any() }
|
||||
}
|
||||
|
||||
@@ -116,7 +116,8 @@ class ExconHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for Excon. */
|
||||
private class ExconDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class ExconDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
ExconDisablesCertificateValidationConfiguration() {
|
||||
this = "ExconDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ class FaradayHttpRequest extends Http::Client::Request::Range, DataFlow::CallNod
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for Faraday. */
|
||||
private class FaradayDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class FaradayDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
FaradayDisablesCertificateValidationConfiguration() {
|
||||
this = "FaradayDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ class HttpClientRequest extends Http::Client::Request::Range, DataFlow::CallNode
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for HttpClient. */
|
||||
private class HttpClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class HttpClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
HttpClientDisablesCertificateValidationConfiguration() {
|
||||
this = "HttpClientDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -66,7 +66,8 @@ class HttpartyRequest extends Http::Client::Request::Range, DataFlow::CallNode {
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for Httparty. */
|
||||
private class HttpartyDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class HttpartyDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
HttpartyDisablesCertificateValidationConfiguration() {
|
||||
this = "HttpartyDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ class NetHttpRequest extends Http::Client::Request::Range, DataFlow::CallNode {
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for NetHttp. */
|
||||
private class NetHttpDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class NetHttpDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
NetHttpDisablesCertificateValidationConfiguration() {
|
||||
this = "NetHttpDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -62,7 +62,8 @@ class OpenUriRequest extends Http::Client::Request::Range, DataFlow::CallNode {
|
||||
* Kernel.open("http://example.com").read
|
||||
* ```
|
||||
*/
|
||||
class OpenUriKernelOpenRequest extends Http::Client::Request::Range, DataFlow::CallNode instanceof KernelMethodCall {
|
||||
class OpenUriKernelOpenRequest extends Http::Client::Request::Range, DataFlow::CallNode instanceof KernelMethodCall
|
||||
{
|
||||
OpenUriKernelOpenRequest() { this.getMethodName() = "open" }
|
||||
|
||||
override DataFlow::Node getAUrlPart() { result = this.getArgument(0) }
|
||||
@@ -102,7 +103,8 @@ class OpenUriKernelOpenRequest extends Http::Client::Request::Range, DataFlow::C
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for OpenURI. */
|
||||
private class OpenUriDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class OpenUriDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
OpenUriDisablesCertificateValidationConfiguration() {
|
||||
this = "OpenUriDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ class RestClientHttpRequest extends Http::Client::Request::Range, DataFlow::Call
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for RestClient. */
|
||||
private class RestClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class RestClientDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
RestClientDisablesCertificateValidationConfiguration() {
|
||||
this = "RestClientDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@ class TyphoeusHttpRequest extends Http::Client::Request::Range, DataFlow::CallNo
|
||||
}
|
||||
|
||||
/** A configuration to track values that can disable certificate validation for Typhoeus. */
|
||||
private class TyphoeusDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration {
|
||||
private class TyphoeusDisablesCertificateValidationConfiguration extends DataFlowImplForHttpClientLibraries::Configuration
|
||||
{
|
||||
TyphoeusDisablesCertificateValidationConfiguration() {
|
||||
this = "TyphoeusDisablesCertificateValidationConfiguration"
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ module Pathname {
|
||||
}
|
||||
|
||||
private class PathnamePermissionModification extends FileSystemPermissionModification::Range,
|
||||
PathnameCall {
|
||||
PathnameCall
|
||||
{
|
||||
private DataFlow::Node permissionArg;
|
||||
|
||||
PathnamePermissionModification() {
|
||||
|
||||
@@ -557,7 +557,8 @@ private class CipherNode extends DataFlow::Node {
|
||||
|
||||
/** An operation using the OpenSSL library that uses a cipher. */
|
||||
private class CipherOperation extends Cryptography::CryptographicOperation::Range,
|
||||
DataFlow::CallNode {
|
||||
DataFlow::CallNode
|
||||
{
|
||||
private CipherNode cipherNode;
|
||||
|
||||
CipherOperation() {
|
||||
@@ -587,7 +588,8 @@ private module Digest {
|
||||
private import codeql.ruby.ApiGraphs
|
||||
|
||||
/** A call that hashes some input using a hashing algorithm from the `OpenSSL::Digest` module. */
|
||||
private class DigestCall extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode {
|
||||
private class DigestCall extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
Cryptography::HashingAlgorithm algo;
|
||||
|
||||
DigestCall() {
|
||||
@@ -612,7 +614,8 @@ private module Digest {
|
||||
}
|
||||
|
||||
/** A call to `OpenSSL::Digest.digest` that hashes input directly without constructing a digest instance. */
|
||||
private class DigestCallDirect extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode {
|
||||
private class DigestCallDirect extends Cryptography::CryptographicOperation::Range instanceof DataFlow::CallNode
|
||||
{
|
||||
Cryptography::HashingAlgorithm algo;
|
||||
|
||||
DigestCallDirect() {
|
||||
|
||||
@@ -57,5 +57,6 @@ module PathInjection {
|
||||
* sanitizer-guard.
|
||||
*/
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier { }
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
}
|
||||
|
||||
@@ -51,5 +51,6 @@ module SqlInjection {
|
||||
* sanitizer-guard.
|
||||
*/
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier { }
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@ module StoredXss {
|
||||
import XSS::StoredXss
|
||||
|
||||
/**
|
||||
* DEPRECATED.
|
||||
*
|
||||
* A taint-tracking configuration for reasoning about Stored XSS.
|
||||
*/
|
||||
class Configuration extends TaintTracking::Configuration {
|
||||
deprecated class Configuration extends TaintTracking::Configuration {
|
||||
Configuration() { this = "StoredXss" }
|
||||
|
||||
override predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
@@ -38,6 +40,23 @@ module StoredXss {
|
||||
isAdditionalXssTaintStep(node1, node2)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for reasoning about Stored XSS.
|
||||
*/
|
||||
private module Config implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof Source }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
|
||||
|
||||
predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }
|
||||
|
||||
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
isAdditionalXssTaintStep(node1, node2)
|
||||
}
|
||||
}
|
||||
|
||||
import TaintTracking::Make<Config>
|
||||
}
|
||||
|
||||
/** DEPRECATED: Alias for StoredXss */
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
private import codeql.ruby.AST
|
||||
private import codeql.ruby.DataFlow
|
||||
private import codeql.ruby.DataFlow2
|
||||
private import codeql.ruby.CFG
|
||||
private import codeql.ruby.Concepts
|
||||
private import codeql.ruby.Frameworks
|
||||
@@ -90,7 +89,8 @@ private module Shared {
|
||||
* tag.
|
||||
*/
|
||||
class ArgumentInterpretedAsUrlAsSink extends Sink, ErbOutputMethodCallArgumentNode,
|
||||
ActionView::ArgumentInterpretedAsUrl { }
|
||||
ActionView::ArgumentInterpretedAsUrl
|
||||
{ }
|
||||
|
||||
/**
|
||||
* A argument to a call to the `link_to` method, which does not expect
|
||||
@@ -129,13 +129,15 @@ private module Shared {
|
||||
* An inclusion check against an array of constant strings, considered as a sanitizer-guard.
|
||||
*/
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier { }
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
|
||||
/**
|
||||
* A `VariableWriteAccessCfgNode` that is not succeeded (locally) by another
|
||||
* write to that variable.
|
||||
*/
|
||||
private class FinalInstanceVarWrite extends CfgNodes::ExprNodes::InstanceVariableWriteAccessCfgNode {
|
||||
private class FinalInstanceVarWrite extends CfgNodes::ExprNodes::InstanceVariableWriteAccessCfgNode
|
||||
{
|
||||
private InstanceVariable var;
|
||||
|
||||
FinalInstanceVarWrite() {
|
||||
@@ -291,20 +293,18 @@ private module OrmTracking {
|
||||
/**
|
||||
* A data flow configuration to track flow from finder calls to field accesses.
|
||||
*/
|
||||
class Configuration extends DataFlow2::Configuration {
|
||||
Configuration() { this = "OrmTracking" }
|
||||
|
||||
override predicate isSource(DataFlow2::Node source) { source instanceof OrmInstantiation }
|
||||
private module Config implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof OrmInstantiation }
|
||||
|
||||
// Select any call receiver and narrow down later
|
||||
override predicate isSink(DataFlow2::Node sink) {
|
||||
sink = any(DataFlow2::CallNode c).getReceiver()
|
||||
}
|
||||
predicate isSink(DataFlow::Node sink) { sink = any(DataFlow::CallNode c).getReceiver() }
|
||||
|
||||
override predicate isAdditionalFlowStep(DataFlow2::Node node1, DataFlow2::Node node2) {
|
||||
predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
|
||||
Shared::isAdditionalXssFlowStep(node1, node2)
|
||||
}
|
||||
}
|
||||
|
||||
import DataFlow::Make<Config>
|
||||
}
|
||||
|
||||
/** Provides default sources, sinks and sanitizers for detecting stored cross-site scripting (XSS) vulnerabilities. */
|
||||
@@ -333,10 +333,10 @@ module StoredXss {
|
||||
/** DEPRECATED: Alias for isAdditionalXssTaintStep */
|
||||
deprecated predicate isAdditionalXSSTaintStep = isAdditionalXssTaintStep/2;
|
||||
|
||||
private class OrmFieldAsSource extends Source instanceof DataFlow2::CallNode {
|
||||
private class OrmFieldAsSource extends Source instanceof DataFlow::CallNode {
|
||||
OrmFieldAsSource() {
|
||||
exists(OrmTracking::Configuration subConfig, DataFlow2::CallNode subSrc |
|
||||
subConfig.hasFlow(subSrc, this.getReceiver()) and
|
||||
exists(DataFlow::CallNode subSrc |
|
||||
OrmTracking::hasFlow(subSrc, this.getReceiver()) and
|
||||
subSrc.(OrmInstantiation).methodCallMayAccessField(this.getMethodName())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ module RegExpInjection {
|
||||
* sanitizer-guard.
|
||||
*/
|
||||
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
|
||||
StringConstArrayInclusionCallBarrier { }
|
||||
StringConstArrayInclusionCallBarrier
|
||||
{ }
|
||||
|
||||
/**
|
||||
* A call to `Regexp.escape` (or its alias, `Regexp.quote`), considered as a
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/ruby-all
|
||||
version: 0.5.4-dev
|
||||
version: 0.5.5-dev
|
||||
groups: ruby
|
||||
extractor: ruby
|
||||
dbscheme: ruby.dbscheme
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
## 0.5.4
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 0.5.3
|
||||
|
||||
### New Queries
|
||||
|
||||
4
ruby/ql/src/change-notes/2023-02-17-zip-slip-query.md
Normal file
4
ruby/ql/src/change-notes/2023-02-17-zip-slip-query.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* Added a new query, `rb/zip-slip`, to detect arbitrary file writes during extraction of zip/tar archives.
|
||||
3
ruby/ql/src/change-notes/released/0.5.4.md
Normal file
3
ruby/ql/src/change-notes/released/0.5.4.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.5.4
|
||||
|
||||
No user-facing changes.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.5.3
|
||||
lastReleaseVersion: 0.5.4
|
||||
|
||||
79
ruby/ql/src/experimental/cwe-022-zipslip/ZipSlip.qhelp
Normal file
79
ruby/ql/src/experimental/cwe-022-zipslip/ZipSlip.qhelp
Normal file
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>Extracting files from a malicious tar archive without validating that the destination file path
|
||||
is within the destination directory can cause files outside the destination directory to be
|
||||
overwritten, due to the possible presence of directory traversal elements (<code>..</code>) in
|
||||
archive paths.</p>
|
||||
|
||||
<p>Tar archives contain archive entries representing each file in the archive. These entries
|
||||
include a file path for the entry, but these file paths are not restricted and may contain
|
||||
unexpected special elements such as the directory traversal element (<code>..</code>). If these
|
||||
file paths are used to determine an output file to write the contents of the archive item to, then
|
||||
the file may be written to an unexpected location. This can result in sensitive information being
|
||||
revealed or deleted, or an attacker being able to influence behavior by modifying unexpected
|
||||
files.</p>
|
||||
|
||||
<p>For example, if a tar archive contains a file entry <code>..\sneaky-file</code>, and the tar archive
|
||||
is extracted to the directory <code>c:\output</code>, then naively combining the paths would result
|
||||
in an output file path of <code>c:\output\..\sneaky-file</code>, which would cause the file to be
|
||||
written to <code>c:\sneaky-file</code>.</p>
|
||||
|
||||
</overview>
|
||||
<recommendation>
|
||||
|
||||
<p>Ensure that output paths constructed from tar archive entries are validated
|
||||
to prevent writing files to unexpected locations.</p>
|
||||
|
||||
<p>The recommended way of writing an output file from a tar archive entry is to check that
|
||||
<code>".."</code> does not occur in the path.
|
||||
</p>
|
||||
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>
|
||||
In this example an archive is extracted without validating file paths.
|
||||
If <code>archive.tar</code> contained relative paths (for
|
||||
instance, if it were created by something like <code>tar -cf archive.tar
|
||||
../file.txt</code>) then executing this code could write to locations
|
||||
outside the destination directory.
|
||||
</p>
|
||||
|
||||
<sample src="examples/zip_slip_bad.rb" />
|
||||
|
||||
<p>To fix this vulnerability, we need to check that the path does not
|
||||
contain any <code>".."</code> elements in it.
|
||||
</p>
|
||||
|
||||
<sample src="examples/zip_slip_good.rb" />
|
||||
|
||||
</example>
|
||||
<references>
|
||||
|
||||
<li>
|
||||
Snyk:
|
||||
<a href="https://snyk.io/research/zip-slip-vulnerability">Zip Slip Vulnerability</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://owasp.org/www-community/attacks/Path_Traversal">Path Traversal</a>.
|
||||
</li>
|
||||
<li>
|
||||
class
|
||||
<a href="https://docs.ruby-lang.org/en/2.4.0/Gem/Package/TarReader.html">Gem::Package::TarReader</a>.
|
||||
</li>
|
||||
<li>
|
||||
class
|
||||
<a href="https://ruby-doc.org/stdlib-2.4.0/libdoc/zlib/rdoc/Zlib/GzipReader.html">Zlib::GzipReader</a>.
|
||||
</li>
|
||||
<li>
|
||||
class
|
||||
<a href="https://www.rubydoc.info/github/rubyzip/rubyzip/Zip/File">Zip::File</a>.
|
||||
</li>
|
||||
|
||||
</references>
|
||||
</qhelp>
|
||||
22
ruby/ql/src/experimental/cwe-022-zipslip/ZipSlip.ql
Normal file
22
ruby/ql/src/experimental/cwe-022-zipslip/ZipSlip.ql
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @name Arbitrary file write during zipfile/tarfile extraction
|
||||
* @description Extracting files from a malicious tar archive without validating that the
|
||||
* destination file path is within the destination directory can cause files outside
|
||||
* the destination directory to be overwritten.
|
||||
* @kind path-problem
|
||||
* @id rb/zip-slip
|
||||
* @problem.severity error
|
||||
* @security-severity 7.5
|
||||
* @precision medium
|
||||
* @tags security
|
||||
* external/cwe/cwe-022
|
||||
*/
|
||||
|
||||
import ruby
|
||||
import codeql.ruby.experimental.ZipSlipQuery
|
||||
import DataFlow::PathGraph
|
||||
|
||||
from Configuration cfg, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where cfg.hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "This file extraction depends on a $@.", source.getNode(),
|
||||
"potentially untrusted source"
|
||||
@@ -0,0 +1,21 @@
|
||||
class FilesController < ActionController::Base
|
||||
def zipFileUnsafe
|
||||
path = params[:path]
|
||||
Zip::File.open(path).each do |entry|
|
||||
File.open(entry.name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tarReaderUnsafe
|
||||
path = params[:path]
|
||||
file_stream = IO.new(IO.sysopen(path))
|
||||
tarfile = Gem::Package::TarReader.new(file_stream)
|
||||
tarfile.each do |entry|
|
||||
::File.open(entry.full_name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
class FilesController < ActionController::Base
|
||||
def zipFileSafe
|
||||
path = params[:path]
|
||||
Zip::File.open(path).each do |entry|
|
||||
entry_path = entry.name
|
||||
next if !File.expand_path(entry_path).start_with?('/safepath/')
|
||||
File.open(entry_path, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def tarReaderSafe
|
||||
path = params[:path]
|
||||
file_stream = IO.new(IO.sysopen(path))
|
||||
tarfile = Gem::Package::TarReader.new(file_stream)
|
||||
tarfile.each do |entry|
|
||||
entry_path = entry.full_name
|
||||
raise ExtractFailed if entry_path != "/safepath"
|
||||
::File.open(entry_path, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/ruby-queries
|
||||
version: 0.5.4-dev
|
||||
version: 0.5.5-dev
|
||||
groups:
|
||||
- ruby
|
||||
- queries
|
||||
|
||||
@@ -19,7 +19,8 @@ private import codeql.regex.MissingRegExpAnchor as MissingRegExpAnchor
|
||||
private import codeql.ruby.regexp.RegExpTreeView::RegexTreeView as TreeImpl
|
||||
|
||||
private module Impl implements
|
||||
MissingRegExpAnchor::MissingRegExpAnchorSig<TreeImpl, HostnameRegexp::Impl> {
|
||||
MissingRegExpAnchor::MissingRegExpAnchorSig<TreeImpl, HostnameRegexp::Impl>
|
||||
{
|
||||
predicate isUsedAsReplace(RegExpPatternSource pattern) {
|
||||
exists(DataFlow::CallNode mcn, DataFlow::Node arg, string name |
|
||||
name = mcn.getMethodName() and
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
import codeql.ruby.AST
|
||||
import codeql.ruby.security.StoredXSSQuery
|
||||
import DataFlow::PathGraph
|
||||
import StoredXss::PathGraph
|
||||
|
||||
from StoredXss::Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
|
||||
where config.hasFlowPath(source, sink)
|
||||
from StoredXss::PathNode source, StoredXss::PathNode sink
|
||||
where StoredXss::hasFlowPath(source, sink)
|
||||
select sink.getNode(), source, sink, "Stored cross-site scripting vulnerability due to $@.",
|
||||
source.getNode(), "stored value"
|
||||
|
||||
@@ -1 +1 @@
|
||||
| src/not_ruby.rb:5:25:5:26 | A parse error occurred. Check the syntax of the file using the ruby -c command. If the file is invalid, correct the error or exclude the file from analysis. | Extraction failed in src/not_ruby.rb with error A parse error occurred. Check the syntax of the file using the ruby -c command. If the file is invalid, correct the error or exclude the file from analysis. | 2 |
|
||||
| src/not_ruby.rb:5:25:5:26 | A parse error occurred. Check the syntax of the file. If the file is invalid, correct the error or exclude the file from analysis. | Extraction failed in src/not_ruby.rb with error A parse error occurred. Check the syntax of the file. If the file is invalid, correct the error or exclude the file from analysis. | 2 |
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
edges
|
||||
| zip_slip.rb:8:15:8:54 | call to new : | zip_slip.rb:9:5:9:11 | tarfile : |
|
||||
| zip_slip.rb:9:5:9:11 | tarfile : | zip_slip.rb:9:22:9:26 | entry : |
|
||||
| zip_slip.rb:9:22:9:26 | entry : | zip_slip.rb:10:19:10:33 | call to full_name |
|
||||
| zip_slip.rb:20:50:20:56 | tarfile : | zip_slip.rb:21:7:21:13 | tarfile : |
|
||||
| zip_slip.rb:21:7:21:13 | tarfile : | zip_slip.rb:21:30:21:34 | entry : |
|
||||
| zip_slip.rb:21:30:21:34 | entry : | zip_slip.rb:22:21:22:35 | call to full_name |
|
||||
| zip_slip.rb:46:5:46:24 | call to open : | zip_slip.rb:46:35:46:39 | entry : |
|
||||
| zip_slip.rb:46:35:46:39 | entry : | zip_slip.rb:47:17:47:26 | call to name |
|
||||
| zip_slip.rb:56:30:56:37 | zip_file : | zip_slip.rb:57:7:57:14 | zip_file : |
|
||||
| zip_slip.rb:57:7:57:14 | zip_file : | zip_slip.rb:57:25:57:29 | entry : |
|
||||
| zip_slip.rb:57:25:57:29 | entry : | zip_slip.rb:58:19:58:28 | call to name |
|
||||
| zip_slip.rb:90:12:90:54 | call to open : | zip_slip.rb:91:11:91:14 | gzip : |
|
||||
| zip_slip.rb:91:11:91:14 | gzip : | zip_slip.rb:97:42:97:56 | compressed_file : |
|
||||
| zip_slip.rb:97:42:97:56 | compressed_file : | zip_slip.rb:98:7:98:21 | compressed_file : |
|
||||
| zip_slip.rb:98:7:98:21 | compressed_file : | zip_slip.rb:98:32:98:36 | entry : |
|
||||
| zip_slip.rb:98:32:98:36 | entry : | zip_slip.rb:100:21:100:30 | entry_path |
|
||||
| zip_slip.rb:123:12:123:34 | call to new : | zip_slip.rb:124:7:124:8 | gz : |
|
||||
| zip_slip.rb:124:7:124:8 | gz : | zip_slip.rb:124:19:124:23 | entry : |
|
||||
| zip_slip.rb:124:19:124:23 | entry : | zip_slip.rb:126:21:126:30 | entry_path |
|
||||
nodes
|
||||
| zip_slip.rb:8:15:8:54 | call to new : | semmle.label | call to new : |
|
||||
| zip_slip.rb:9:5:9:11 | tarfile : | semmle.label | tarfile : |
|
||||
| zip_slip.rb:9:22:9:26 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:10:19:10:33 | call to full_name | semmle.label | call to full_name |
|
||||
| zip_slip.rb:20:50:20:56 | tarfile : | semmle.label | tarfile : |
|
||||
| zip_slip.rb:21:7:21:13 | tarfile : | semmle.label | tarfile : |
|
||||
| zip_slip.rb:21:30:21:34 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:22:21:22:35 | call to full_name | semmle.label | call to full_name |
|
||||
| zip_slip.rb:46:5:46:24 | call to open : | semmle.label | call to open : |
|
||||
| zip_slip.rb:46:35:46:39 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:47:17:47:26 | call to name | semmle.label | call to name |
|
||||
| zip_slip.rb:56:30:56:37 | zip_file : | semmle.label | zip_file : |
|
||||
| zip_slip.rb:57:7:57:14 | zip_file : | semmle.label | zip_file : |
|
||||
| zip_slip.rb:57:25:57:29 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:58:19:58:28 | call to name | semmle.label | call to name |
|
||||
| zip_slip.rb:90:12:90:54 | call to open : | semmle.label | call to open : |
|
||||
| zip_slip.rb:91:11:91:14 | gzip : | semmle.label | gzip : |
|
||||
| zip_slip.rb:97:42:97:56 | compressed_file : | semmle.label | compressed_file : |
|
||||
| zip_slip.rb:98:7:98:21 | compressed_file : | semmle.label | compressed_file : |
|
||||
| zip_slip.rb:98:32:98:36 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:100:21:100:30 | entry_path | semmle.label | entry_path |
|
||||
| zip_slip.rb:123:12:123:34 | call to new : | semmle.label | call to new : |
|
||||
| zip_slip.rb:124:7:124:8 | gz : | semmle.label | gz : |
|
||||
| zip_slip.rb:124:19:124:23 | entry : | semmle.label | entry : |
|
||||
| zip_slip.rb:126:21:126:30 | entry_path | semmle.label | entry_path |
|
||||
subpaths
|
||||
#select
|
||||
| zip_slip.rb:10:19:10:33 | call to full_name | zip_slip.rb:8:15:8:54 | call to new : | zip_slip.rb:10:19:10:33 | call to full_name | This file extraction depends on a $@. | zip_slip.rb:8:15:8:54 | call to new | potentially untrusted source |
|
||||
| zip_slip.rb:22:21:22:35 | call to full_name | zip_slip.rb:20:50:20:56 | tarfile : | zip_slip.rb:22:21:22:35 | call to full_name | This file extraction depends on a $@. | zip_slip.rb:20:50:20:56 | tarfile | potentially untrusted source |
|
||||
| zip_slip.rb:47:17:47:26 | call to name | zip_slip.rb:46:5:46:24 | call to open : | zip_slip.rb:47:17:47:26 | call to name | This file extraction depends on a $@. | zip_slip.rb:46:5:46:24 | call to open | potentially untrusted source |
|
||||
| zip_slip.rb:58:19:58:28 | call to name | zip_slip.rb:56:30:56:37 | zip_file : | zip_slip.rb:58:19:58:28 | call to name | This file extraction depends on a $@. | zip_slip.rb:56:30:56:37 | zip_file | potentially untrusted source |
|
||||
| zip_slip.rb:100:21:100:30 | entry_path | zip_slip.rb:90:12:90:54 | call to open : | zip_slip.rb:100:21:100:30 | entry_path | This file extraction depends on a $@. | zip_slip.rb:90:12:90:54 | call to open | potentially untrusted source |
|
||||
| zip_slip.rb:126:21:126:30 | entry_path | zip_slip.rb:123:12:123:34 | call to new : | zip_slip.rb:126:21:126:30 | entry_path | This file extraction depends on a $@. | zip_slip.rb:123:12:123:34 | call to new | potentially untrusted source |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/cwe-022-zipslip/ZipSlip.ql
|
||||
@@ -0,0 +1,133 @@
|
||||
require 'zip'
|
||||
|
||||
class TestController < ActionController::Base
|
||||
# BAD
|
||||
def tarReaderUnsafe
|
||||
path = params[:path]
|
||||
file_stream = IO.new(IO.sysopen(path))
|
||||
tarfile = Gem::Package::TarReader.new(file_stream)
|
||||
tarfile.each do |entry|
|
||||
::File.open(entry.full_name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# BAD
|
||||
def tarReaderBlockUnsafe
|
||||
path = params[:path]
|
||||
file_stream = IO.new(IO.sysopen(path))
|
||||
Gem::Package::TarReader.new(file_stream) do |tarfile|
|
||||
tarfile.each_entry do |entry|
|
||||
::File.open(entry.full_name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GOOD
|
||||
def tarReadeSanitizedExpandPath
|
||||
path = params[:path]
|
||||
file_stream = IO.new(IO.sysopen(path))
|
||||
tarfile = Gem::Package::TarReader.new(file_stream)
|
||||
tarfile.each do |entry|
|
||||
entry_path = entry.full_name
|
||||
next if !File.expand_path(entry_path).start_with?("/safepath/")
|
||||
::File.open(entry_path, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# BAD
|
||||
def zipFileUnsafe
|
||||
path = params[:path]
|
||||
Zip::File.open(path).each do |entry|
|
||||
File.open(entry.name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# BAD
|
||||
def zipFileBlockUnsafe
|
||||
path = params[:path]
|
||||
Zip::File.open(path) do |zip_file|
|
||||
zip_file.each do |entry|
|
||||
File.open(entry.name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GOOD
|
||||
def zipFileBlockSafeHardcodedPath
|
||||
path = '/safepath.zip'
|
||||
Zip::File.open(path) do |zip_file|
|
||||
zip_file.each do |entry|
|
||||
File.open(entry.name, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GOOD
|
||||
def zipFileSanitizedConstCompare
|
||||
path = params[:path]
|
||||
Zip::File.open(path).each do |entry|
|
||||
entry_path = entry.name
|
||||
raise ExtractFailed if entry_path != "/safepath"
|
||||
File.open(entry_path, "wb") do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_compressed_file_stream(compressed_file_path)
|
||||
gzip = Zlib::GzipReader.open(compressed_file_path)
|
||||
yield(gzip)
|
||||
end
|
||||
|
||||
# BAD
|
||||
def gzipReaderUnsafe
|
||||
path = params[:path]
|
||||
get_compressed_file_stream(path) do |compressed_file|
|
||||
compressed_file.each do |entry|
|
||||
entry_path = entry.full_name
|
||||
::File.open(entry_path, 'wb') do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# GOOD
|
||||
def gzipReaderSafeConstPath
|
||||
path = "/safe.zip"
|
||||
zlib = Zlib::GzipReader.open(path)
|
||||
zlib.each do |entry|
|
||||
entry_path = entry.full_name
|
||||
::File.open(entry_path, 'wb') do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# BAD
|
||||
def gzipReaderUnsafeNewInstance
|
||||
path = params[:path]
|
||||
File.open(path, 'rb') do |f|
|
||||
gz = Zlib::GzipReader.new(f)
|
||||
gz.each do |entry|
|
||||
entry_path = entry.full_name
|
||||
::File.open(entry_path, 'wb') do |os|
|
||||
entry.read
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Reference in New Issue
Block a user