Merge pull request #14679 from hmac/hmac-model-editor-ruby

Ruby: Experimental model editor support
This commit is contained in:
Harry Maclean
2023-12-08 11:03:38 +00:00
committed by GitHub
19 changed files with 314 additions and 1 deletions

View File

@@ -57,7 +57,10 @@ module Gem {
}
/** Gets the name of the gem */
string getName() { result = this.getSpecProperty("name").getConstantValue().getString() }
string getName() {
result = this.getSpecProperty("name").getConstantValue().getString() or
result = specCall.getArgument(0).getAValueReachingSink().getConstantValue().getString()
}
/** Gets a path that is loaded when the gem is required */
private string getARequirePath() {

View File

@@ -4,6 +4,7 @@ private import codeql.ruby.Concepts
private import codeql.ruby.Frameworks
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
/**
* Provides default sources, sinks and sanitizers for detecting
@@ -156,4 +157,8 @@ module CodeInjection {
override FlowState::State getAState() { result instanceof FlowState::Full }
}
private class ExternalCodeInjectionSink extends Sink {
ExternalCodeInjectionSink() { this = ModelOutput::getASinkNode("code-injection").asSink() }
}
}

View File

@@ -9,6 +9,7 @@ private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.Concepts
private import codeql.ruby.Frameworks
private import codeql.ruby.ApiGraphs
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
module CommandInjection {
/**
@@ -52,4 +53,10 @@ module CommandInjection {
this.(DataFlow::CallNode).getMethodName() = "shellescape"
}
}
private class ExternalCommandInjectionSink extends Sink {
ExternalCommandInjectionSink() {
this = ModelOutput::getASinkNode("command-injection").asSink()
}
}
}

View File

@@ -8,6 +8,7 @@ import codeql.ruby.DataFlow
import codeql.ruby.TaintTracking
import codeql.ruby.dataflow.RemoteFlowSources
import codeql.ruby.frameworks.Core
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
/**
* A data flow source for user input used in log entries.
@@ -50,6 +51,10 @@ class LoggingSink extends Sink {
LoggingSink() { this = any(Logging logging).getAnInput() }
}
private class ExternalLogInjectionSink extends Sink {
ExternalLogInjectionSink() { this = ModelOutput::getASinkNode("log-injection").asSink() }
}
/**
* A call to `String#replace` that replaces `\n` is considered to sanitize the replaced string (reduce false positive).
*/

View File

@@ -11,6 +11,7 @@ private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
module PathInjection {
/**
@@ -52,4 +53,8 @@ module PathInjection {
class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
StringConstArrayInclusionCallBarrier
{ }
private class ExternalPathInjectionSink extends Sink {
ExternalPathInjectionSink() { this = ModelOutput::getASinkNode("path-injection").asSink() }
}
}

View File

@@ -8,6 +8,7 @@ private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.ApiGraphs
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
/**
* Provides default sources, sinks and sanitizers for detecting SQL injection
@@ -56,4 +57,8 @@ module SqlInjection {
{ }
private class SqlSanitizationAsSanitizer extends Sanitizer, SqlSanitization { }
private class ExternalSqlInjectionSink extends Sink {
ExternalSqlInjectionSink() { this = ModelOutput::getASinkNode("sql-injection").asSink() }
}
}

View File

@@ -11,6 +11,7 @@ private import codeql.ruby.dataflow.RemoteFlowSources
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.Sanitizers
private import codeql.ruby.frameworks.ActionController
private import codeql.ruby.frameworks.data.internal.ApiGraphModels
/**
* Provides default sources, sinks and sanitizers for detecting
@@ -73,6 +74,10 @@ module UrlRedirect {
}
}
private class ExternalUrlRedirectSink extends Sink {
ExternalUrlRedirectSink() { this = ModelOutput::getASinkNode("url-redirection").asSink() }
}
/**
* A comparison with a constant string, considered as a sanitizer-guard.
*/

View File

@@ -1,3 +1,11 @@
/**
* @name Generate flow models
* @description Queries to generate source, sink, summary and type models.
* @kind table
* @id rb/utils/modeleditor/generate-model
* @tags modeleditor generate-model framework-mode
*/
private import internal.Types
private import internal.Summaries

View File

@@ -0,0 +1,17 @@
/**
* @name Fetch endpoints for use in the model editor (application mode)
* @description A list of 3rd party endpoints (methods and attributes) used in the codebase. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/application-mode-endpoints
* @tags modeleditor endpoints application-mode
*/
import codeql.ruby.AST
// This query is empty as Application Mode is not yet supported for Ruby.
from
Call usage, string package, string type, string name, string parameters, boolean supported,
string namespace, string version, string supportedType, string classification
where none()
select usage, package, namespace, type, name, parameters, supported, namespace, version,
supportedType, classification

View File

@@ -0,0 +1,15 @@
/**
* @name Fetch endpoints for use in the model editor (framework mode)
* @description A list of endpoints accessible (methods and attributes) for consumers of the library. Excludes test and generated code.
* @kind table
* @id rb/utils/modeleditor/framework-mode-endpoints
* @tags modeleditor endpoints framework-mode
*/
import ruby
import ModelEditor
from PublicEndpointFromSource endpoint
select endpoint, endpoint.getNamespace(), endpoint.getTypeName(), endpoint.getName(),
endpoint.getParameterTypes(), endpoint.getSupportedStatus(), endpoint.getFile().getBaseName(),
endpoint.getSupportedType()

View File

@@ -0,0 +1,173 @@
/** Provides classes and predicates related to handling APIs for the VS Code extension. */
private import ruby
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.dataflow.internal.DataFlowPrivate
private import codeql.ruby.dataflow.internal.FlowSummaryImpl as FlowSummaryImpl
private import codeql.ruby.dataflow.internal.FlowSummaryImplSpecific
private import codeql.ruby.frameworks.core.Gem
private import codeql.ruby.frameworks.data.ModelsAsData
private import codeql.ruby.frameworks.data.internal.ApiGraphModelsExtensions
private import queries.modeling.internal.Util as Util
/** Holds if the given callable is not worth supporting. */
private predicate isUninteresting(DataFlow::MethodNode c) {
c.getLocation().getFile() instanceof TestFile
}
private predicate gemFileStep(Gem::GemSpec gem, Folder folder, int n) {
n = 0 and folder.getAFile() = gem.(File)
or
exists(Folder parent, int m |
gemFileStep(gem, parent, m) and
parent.getAFolder() = folder and
n = m + 1
)
}
/**
* A callable method or accessor from either the Ruby Standard Library, a 3rd party library, or from the source.
*/
class Endpoint extends DataFlow::MethodNode {
Endpoint() { this.isPublic() and not isUninteresting(this) }
File getFile() { result = this.getLocation().getFile() }
string getName() { result = this.getMethodName() }
/**
* Gets the namespace of this endpoint.
*/
bindingset[this]
string getNamespace() {
exists(Folder folder | folder = this.getFile().getParentContainer() |
// The nearest gemspec to this endpoint, if one exists
result = min(Gem::GemSpec g, int n | gemFileStep(g, folder, n) | g order by n).getName()
or
not gemFileStep(_, folder, _) and
result = ""
)
}
/**
* Gets the unbound type name of this endpoint.
*/
bindingset[this]
string getTypeName() {
result =
any(DataFlow::ModuleNode m | m.getOwnInstanceMethod(this.getMethodName()) = this)
.getQualifiedName() or
result =
any(DataFlow::ModuleNode m | m.getOwnSingletonMethod(this.getMethodName()) = this)
.getQualifiedName() + "!"
}
/**
* Gets the parameter types of this endpoint.
*/
bindingset[this]
string getParameterTypes() {
// For now, return the names of postional and keyword parameters. We don't always have type information, so we can't return type names.
// We don't yet handle splat params or block params.
result =
"(" +
concat(string key, string value |
value = any(int i | i.toString() = key | this.asCallable().getParameter(i)).getName()
or
exists(DataFlow::ParameterNode param |
param = this.asCallable().getKeywordParameter(key)
|
value = key + ":"
)
|
value, "," order by key
) + ")"
}
/** Holds if this API has a supported summary. */
pragma[nomagic]
predicate hasSummary() { none() }
/** Holds if this API is a known source. */
pragma[nomagic]
abstract predicate isSource();
/** Holds if this API is a known sink. */
pragma[nomagic]
abstract predicate isSink();
/** Holds if this API is a known neutral. */
pragma[nomagic]
predicate isNeutral() { none() }
/**
* Holds if this API is supported by existing CodeQL libraries, that is, it is either a
* recognized source, sink or neutral or it has a flow summary.
*/
predicate isSupported() {
this.hasSummary() or this.isSource() or this.isSink() or this.isNeutral()
}
boolean getSupportedStatus() { if this.isSupported() then result = true else result = false }
string getSupportedType() {
this.isSink() and result = "sink"
or
this.isSource() and result = "source"
or
this.hasSummary() and result = "summary"
or
this.isNeutral() and result = "neutral"
or
not this.isSupported() and result = ""
}
}
string methodClassification(Call method) {
method.getFile() instanceof TestFile and result = "test"
or
not method.getFile() instanceof TestFile and
result = "source"
}
class TestFile extends File {
TestFile() {
this.getRelativePath().regexpMatch(".*(test|spec).+") and
not this.getAbsolutePath().matches("%/ql/test/%") // allows our test cases to work
}
}
/**
* A callable where there exists a MaD sink model that applies to it.
*/
class SinkCallable extends DataFlow::MethodNode {
SinkCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sinkModel(type, path, _)
)
}
}
/**
* A callable where there exists a MaD source model that applies to it.
*/
class SourceCallable extends DataFlow::CallableNode {
SourceCallable() {
exists(string type, string path, string method |
method = path.regexpCapture("(Method\\[[^\\]]+\\]).*", 1) and
Util::pathToMethod(this, type, method) and
sourceModel(type, path, _)
)
}
}
/**
* A class of effectively public callables from source code.
*/
class PublicEndpointFromSource extends Endpoint {
override predicate isSource() { this instanceof SourceCallable }
override predicate isSink() { this instanceof SinkCallable }
}

View File

@@ -0,0 +1,8 @@
| lib/module.rb:2:3:3:5 | foo | mylib | M1 | foo | (x,y) | false | module.rb | |
| lib/module.rb:5:3:6:5 | self_foo | mylib | M1! | self_foo | (x,y) | false | module.rb | |
| lib/mylib.rb:4:3:5:5 | foo | mylib | A | foo | (x,y,key1:) | false | mylib.rb | |
| lib/mylib.rb:7:3:8:5 | bar | mylib | A | bar | (x) | false | mylib.rb | |
| lib/mylib.rb:10:3:11:5 | self_foo | mylib | A! | self_foo | (x,y) | false | mylib.rb | |
| lib/mylib.rb:19:5:20:7 | foo | mylib | A::ANested | foo | (x,y) | false | mylib.rb | |
| lib/other.rb:6:3:7:5 | foo | mylib | B | foo | (x,y) | false | other.rb | |
| other_lib/lib/other_gem.rb:3:9:4:11 | foo | other-lib | OtherLib::A | foo | (x,y) | false | other_gem.rb | |

View File

@@ -0,0 +1 @@
utils/modeleditor/FrameworkModeEndpoints.ql

View File

@@ -0,0 +1,7 @@
module M1
def foo(x, y)
end
def self.self_foo(x, y)
end
end

View File

@@ -0,0 +1,27 @@
require_relative "./other"
class A
def foo(x, y, key1:, **kwargs, &block)
end
def bar(x, *args)
end
def self.self_foo(x, y)
end
private
def private_1(x, y)
end
class ANested
def foo(x, y)
end
private
def private_2(x, y)
end
end
end

View File

@@ -0,0 +1,12 @@
require_relative "./module"
class B
include M1
def foo(x, y)
end
end
class C
extend M1
end

View File

@@ -0,0 +1,3 @@
Gem::Specification.new do |s|
s.name = "mylib"
end

View File

@@ -0,0 +1,6 @@
module OtherLib
class A
def foo(x, y)
end
end
end

View File

@@ -0,0 +1 @@
Gem::Specification.new("other-lib")