mirror of
https://github.com/github/codeql.git
synced 2026-04-26 01:05:15 +02:00
Ruby: Restrict rb/csrf-protection-not-enabled
This query only applies to codebases using Ruby on Rails < 5.2, or where there is no call to `csrf_meta_tags` in the base ERb template.
This commit is contained in:
243
ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll
Normal file
243
ruby/ql/lib/codeql/ruby/frameworks/Gemfile.qll
Normal file
@@ -0,0 +1,243 @@
|
||||
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() }
|
||||
|
||||
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() }
|
||||
|
||||
/**
|
||||
* 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)
|
||||
)
|
||||
}
|
||||
|
||||
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()) }
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
import codeql.ruby.AST
|
||||
import codeql.ruby.Concepts
|
||||
import codeql.ruby.frameworks.ActionController
|
||||
import codeql.ruby.frameworks.Gemfile
|
||||
|
||||
/**
|
||||
* Holds if a call to `protect_from_forgery` is made in the controller class `definedIn`,
|
||||
@@ -26,6 +27,23 @@ private predicate protectFromForgeryCall(
|
||||
definedIn.getSelf().flowsTo(call.getReceiver()) and child = definedIn.getADescendent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds if the Gemfile for this application specifies a version of "rails" < 3.0.0.
|
||||
* Rails versions from 3.0.0 onwards enable CSRF protection by default.
|
||||
*/
|
||||
private predicate railsPreVersion3() {
|
||||
exists(Gemfile::Gem g | g.getName() = "rails" and g.getAVersionConstraint().before("5.2"))
|
||||
}
|
||||
|
||||
from ActionControllerClass c
|
||||
where not protectFromForgeryCall(_, c, _)
|
||||
where
|
||||
not protectFromForgeryCall(_, c, _) and
|
||||
// Rails versions prior to 3.0.0 require CSRF protection to be explicitly enabled.
|
||||
// For later versions, there must exist a call to `csrf_meta_tags` in every HTML response.
|
||||
// We currently just check for a call to this method anywhere in the codebase.
|
||||
(
|
||||
railsPreVersion3()
|
||||
or
|
||||
not any(MethodCall m).getMethodName() = "csrf_meta_tags"
|
||||
)
|
||||
select c, "Potential CSRF vulnerability due to forgery protection not being enabled."
|
||||
|
||||
Reference in New Issue
Block a user