mirror of
https://github.com/github/codeql.git
synced 2026-05-05 13:45:19 +02:00
Merge pull request #14308 from hmac/hmac-rb-csrf-not-enabled
Ruby: Add a query for CSRF protection not enabled
This commit is contained in:
@@ -22,6 +22,35 @@ private import codeql.ruby.dataflow.internal.DataFlowDispatch
|
||||
module ActionController {
|
||||
// TODO: move the rest of this file inside this module.
|
||||
import codeql.ruby.frameworks.actioncontroller.Filters
|
||||
|
||||
/**
|
||||
* An ActionController class which sits at the top of the class hierarchy.
|
||||
* In other words, it does not subclass any other class in source code.
|
||||
*/
|
||||
class RootController extends ActionControllerClass {
|
||||
RootController() {
|
||||
not exists(ActionControllerClass parent | this != parent and this = parent.getADescendent())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `protect_from_forgery`.
|
||||
*/
|
||||
class ProtectFromForgeryCall extends CsrfProtectionSetting::Range, DataFlow::CallNode {
|
||||
ProtectFromForgeryCall() {
|
||||
this = actionControllerInstance().getAMethodCall("protect_from_forgery")
|
||||
}
|
||||
|
||||
private string getWithValueText() {
|
||||
result = this.getKeywordArgument("with").getConstantValue().getSymbol()
|
||||
}
|
||||
|
||||
// Calls without `with: :exception` can allow for bypassing CSRF protection
|
||||
// in some scenarios.
|
||||
override boolean getVerificationSetting() {
|
||||
if this.getWithValueText() = "exception" then result = true else result = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,18 +68,12 @@ module ActionController {
|
||||
*/
|
||||
class ActionControllerClass extends DataFlow::ClassNode {
|
||||
ActionControllerClass() {
|
||||
this =
|
||||
[
|
||||
DataFlow::getConstant("ActionController").getConstant("Base"),
|
||||
// In Rails applications `ApplicationController` typically extends `ActionController::Base`, but we
|
||||
// treat it separately in case the `ApplicationController` definition is not in the database.
|
||||
DataFlow::getConstant("ApplicationController"),
|
||||
// ActionController::Metal technically doesn't contain all of the
|
||||
// methods available in Base, such as those for rendering views.
|
||||
// However we prefer to be over-sensitive in this case in order to find
|
||||
// more results.
|
||||
DataFlow::getConstant("ActionController").getConstant("Metal")
|
||||
].getADescendentModule()
|
||||
// In Rails applications `ApplicationController` typically extends `ActionController::Base`, but we
|
||||
// treat it separately in case the `ApplicationController` definition is not in the database.
|
||||
this = DataFlow::getConstant("ApplicationController").getADescendentModule()
|
||||
or
|
||||
this = actionControllerBaseClass().getADescendentModule() and
|
||||
not exists(DataFlow::ModuleNode m | m = actionControllerBaseClass().asModule() | this = m)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,6 +97,18 @@ class ActionControllerClass extends DataFlow::ClassNode {
|
||||
}
|
||||
}
|
||||
|
||||
private DataFlow::ConstRef actionControllerBaseClass() {
|
||||
result =
|
||||
[
|
||||
DataFlow::getConstant("ActionController").getConstant("Base"),
|
||||
// ActionController::Metal and ActionController::API technically don't contain all of the
|
||||
// methods available in Base, such as those for rendering views.
|
||||
// However we prefer to be over-sensitive in this case in order to find more results.
|
||||
DataFlow::getConstant("ActionController").getConstant("Metal"),
|
||||
DataFlow::getConstant("ActionController").getConstant("API")
|
||||
]
|
||||
}
|
||||
|
||||
private API::Node actionControllerInstance() {
|
||||
result = any(ActionControllerClass cls).getSelf().track()
|
||||
}
|
||||
@@ -407,27 +442,6 @@ class ActionControllerSkipForgeryProtectionCall extends CsrfProtectionSetting::R
|
||||
override boolean getVerificationSetting() { result = false }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `protect_from_forgery`.
|
||||
*/
|
||||
private class ActionControllerProtectFromForgeryCall extends CsrfProtectionSetting::Range,
|
||||
DataFlow::CallNode
|
||||
{
|
||||
ActionControllerProtectFromForgeryCall() {
|
||||
this = actionControllerInstance().getAMethodCall("protect_from_forgery")
|
||||
}
|
||||
|
||||
private string getWithValueText() {
|
||||
result = this.getKeywordArgument("with").getConstantValue().getSymbol()
|
||||
}
|
||||
|
||||
// Calls without `with: :exception` can allow for bypassing CSRF protection
|
||||
// in some scenarios.
|
||||
override boolean getVerificationSetting() {
|
||||
if this.getWithValueText() = "exception" then result = true else result = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `send_file`, which sends the file at the given path to the client.
|
||||
*/
|
||||
|
||||
254
ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll
Normal file
254
ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll
Normal file
@@ -0,0 +1,254 @@
|
||||
/**
|
||||
* Provides classes and predicates for Gemfiles, including version constraint logic.
|
||||
*/
|
||||
|
||||
private import codeql.ruby.AST
|
||||
|
||||
/**
|
||||
* Provides classes and predicates for Gemfiles, including version constraint logic.
|
||||
*/
|
||||
module Gemfile {
|
||||
private File getGemfile() { result.getBaseName() = "Gemfile" }
|
||||
|
||||
/**
|
||||
* A call to `gem` inside a gemfile. This defines a dependency. For example:
|
||||
*
|
||||
* ```rb
|
||||
* gem "actionpack", "~> 7.0.0"
|
||||
* ```
|
||||
*
|
||||
* This call defines a dependency on the `actionpack` gem, with version constraint `~> 7.0.0`.
|
||||
* For detail on version constraints, see the `VersionConstraint` class.
|
||||
*/
|
||||
class Gem extends MethodCall {
|
||||
Gem() { this.getMethodName() = "gem" and this.getFile() = getGemfile() }
|
||||
|
||||
/**
|
||||
* Gets the name of the gem in this version constraint.
|
||||
*/
|
||||
string getName() { result = this.getArgument(0).getConstantValue().getStringlikeValue() }
|
||||
|
||||
/**
|
||||
* Gets the `i`th version string for this gem. A single `gem` call may have multiple version constraints, for example:
|
||||
*
|
||||
* ```rb
|
||||
* gem "json", "3.4.0", ">= 3.0"
|
||||
* ```
|
||||
*/
|
||||
string getVersionString(int i) {
|
||||
result = this.getArgument(i + 1).getConstantValue().getStringlikeValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a version constraint defined by this call.
|
||||
*/
|
||||
VersionConstraint getAVersionConstraint() { result = this.getVersionString(_) }
|
||||
}
|
||||
|
||||
private newtype TComparator =
|
||||
TEq() or
|
||||
TNeq() or
|
||||
TGt() or
|
||||
TLt() or
|
||||
TGeq() or
|
||||
TLeq() or
|
||||
TPGeq()
|
||||
|
||||
/**
|
||||
* A comparison operator in a version constraint.
|
||||
*/
|
||||
private class Comparator extends TComparator {
|
||||
string toString() { result = this.toSourceString() }
|
||||
|
||||
/**
|
||||
* Gets the representation of the comparator in source code.
|
||||
* This is defined separately so that we can change the `toString` implementation without breaking `parseConstraint`.
|
||||
*/
|
||||
string toSourceString() {
|
||||
this = TEq() and result = "="
|
||||
or
|
||||
this = TNeq() and result = "!="
|
||||
or
|
||||
this = TGt() and result = ">"
|
||||
or
|
||||
this = TLt() and result = "<"
|
||||
or
|
||||
this = TGeq() and result = ">="
|
||||
or
|
||||
this = TLeq() and result = "<="
|
||||
or
|
||||
this = TPGeq() and result = "~>"
|
||||
}
|
||||
}
|
||||
|
||||
bindingset[s]
|
||||
private predicate parseExactVersion(string s, string version) {
|
||||
version = s.regexpCapture("\\s*(\\d+\\.\\d+\\.\\d+)\\s*", 1)
|
||||
}
|
||||
|
||||
bindingset[s]
|
||||
private predicate parseConstraint(string s, Comparator c, string version) {
|
||||
exists(string pattern | pattern = "(=|!=|>=?|<=?|~>)\\s+(.+)" |
|
||||
c.toSourceString() = s.regexpCapture(pattern, 1) and version = s.regexpCapture(pattern, 2)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A version constraint in a `gem` call. This consists of a version number and an optional comparator, for example
|
||||
* `>= 1.2.3`.
|
||||
*/
|
||||
class VersionConstraint extends string {
|
||||
Comparator comp;
|
||||
string versionString;
|
||||
|
||||
VersionConstraint() {
|
||||
this = any(Gem g).getVersionString(_) and
|
||||
(
|
||||
parseConstraint(this, comp, versionString)
|
||||
or
|
||||
parseExactVersion(this, versionString) and comp = TEq()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the string defining the version number used in this constraint.
|
||||
*/
|
||||
string getVersionString() { result = versionString }
|
||||
|
||||
/**
|
||||
* Gets the `Version` used in this constraint.
|
||||
*/
|
||||
Version getVersion() { result = this.getVersionString() }
|
||||
|
||||
/**
|
||||
* Holds if `other` is a version which is strictly greater than the range described by this version constraint.
|
||||
*/
|
||||
bindingset[other]
|
||||
predicate before(string other) {
|
||||
comp = TEq() and this.getVersion().before(other)
|
||||
or
|
||||
comp = TLt() and
|
||||
(this.getVersion().before(other) or this.getVersion().equal(other))
|
||||
or
|
||||
comp = TLeq() and this.getVersion().before(other)
|
||||
or
|
||||
// ~> x.y.z <=> >= x.y.z && < x.(y+1).0
|
||||
// ~> x.y <=> >= x.y && < (x+1).0
|
||||
comp = TPGeq() and
|
||||
exists(int thisMajor, int thisMinor, int otherMajor, int otherMinor |
|
||||
thisMajor = this.getVersion().getMajor() and
|
||||
thisMinor = this.getVersion().getMinor() and
|
||||
exists(string maj, string mi | normalizeSemver(other, _, maj, mi, _) |
|
||||
otherMajor = maj.toInt() and otherMinor = mi.toInt()
|
||||
)
|
||||
|
|
||||
exists(this.getVersion().getPatch()) and
|
||||
(
|
||||
thisMajor < otherMajor
|
||||
or
|
||||
thisMajor = otherMajor and
|
||||
thisMinor < otherMinor
|
||||
)
|
||||
or
|
||||
not exists(this.getVersion().getPatch()) and
|
||||
thisMajor < otherMajor
|
||||
)
|
||||
// if the comparator is > or >=, it has no upper bound and therefore isn't guaranteed to be before any other version.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A version number in a version constraint. For example, in the following code
|
||||
*
|
||||
* ```rb
|
||||
* gem "json", ">= 3.4.5"
|
||||
* ```
|
||||
*
|
||||
* The version is `3.4.5`.
|
||||
*/
|
||||
private class Version extends string {
|
||||
string normalized;
|
||||
|
||||
Version() {
|
||||
this = any(Gem c).getAVersionConstraint().getVersionString() and
|
||||
normalized = normalizeSemver(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if this version is strictly before the version defined by `other`.
|
||||
*/
|
||||
bindingset[other]
|
||||
predicate before(string other) { normalized < normalizeSemver(other) }
|
||||
|
||||
/**
|
||||
* Holds if this versino is equal to the version defined by `other`.
|
||||
*/
|
||||
bindingset[other]
|
||||
predicate equal(string other) { normalized = normalizeSemver(other) }
|
||||
|
||||
/**
|
||||
* Holds if this version is strictly after the version defined by `other`.
|
||||
*/
|
||||
bindingset[other]
|
||||
predicate after(string other) { normalized > normalizeSemver(other) }
|
||||
|
||||
/**
|
||||
* Holds if this version defines a patch number.
|
||||
*/
|
||||
predicate hasPatch() { exists(getPatch(this)) }
|
||||
|
||||
/**
|
||||
* Gets the major number of this version.
|
||||
*/
|
||||
int getMajor() { result = getMajor(normalized).toInt() }
|
||||
|
||||
/**
|
||||
* Gets the minor number of this version, if it exists.
|
||||
*/
|
||||
int getMinor() { result = getMinor(normalized).toInt() }
|
||||
|
||||
/**
|
||||
* Gets the patch number of this version, if it exists.
|
||||
*/
|
||||
int getPatch() { result = getPatch(normalized).toInt() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a SemVer string such that the lexicographical ordering
|
||||
* of two normalized strings is consistent with the SemVer ordering.
|
||||
*
|
||||
* Pre-release information and build metadata is not supported.
|
||||
*/
|
||||
bindingset[orig]
|
||||
private predicate normalizeSemver(
|
||||
string orig, string normalized, string major, string minor, string patch
|
||||
) {
|
||||
major = getMajor(orig) and
|
||||
(
|
||||
minor = getMinor(orig)
|
||||
or
|
||||
not exists(getMinor(orig)) and minor = "0"
|
||||
) and
|
||||
(
|
||||
patch = getPatch(orig)
|
||||
or
|
||||
not exists(getPatch(orig)) and patch = "0"
|
||||
) and
|
||||
normalized = leftPad(major) + "." + leftPad(minor) + "." + leftPad(patch)
|
||||
}
|
||||
|
||||
bindingset[orig]
|
||||
private string normalizeSemver(string orig) { normalizeSemver(orig, result, _, _, _) }
|
||||
|
||||
bindingset[s]
|
||||
private string getMajor(string s) { result = s.regexpCapture("(\\d+).*", 1) }
|
||||
|
||||
bindingset[s]
|
||||
private string getMinor(string s) { result = s.regexpCapture("(\\d+)\\.(\\d+).*", 2) }
|
||||
|
||||
bindingset[s]
|
||||
private string getPatch(string s) { result = s.regexpCapture("(\\d+)\\.(\\d+)\\.(\\d+).*", 3) }
|
||||
|
||||
bindingset[str]
|
||||
private string leftPad(string str) { result = ("000" + str).suffix(str.length()) }
|
||||
}
|
||||
Reference in New Issue
Block a user