-
Notifications
You must be signed in to change notification settings - Fork 736
Creating Custom Brakeman Rules
Brakeman checks are implemented as individual Ruby classes. Each check is run independently (in separate threads by default).
A check may produce any number of warnings, although it's typically best to limit a check to as few warning types as possible to make it easier to enable/disable warnings as desired.
All Brakeman checks look like this:
require 'brakeman/checks/base_check'
class Brakeman::CheckName < Brakeman::BaseCheck
Brakeman::Checks.add self
@description = ""
def run_check
end
end
With annotations:
# Load the base class
require 'brakeman/checks/base_check'
# All checks inherit from Brakeman::BaseCheck
# The name of the class will be used to enable/disable the check
# and will be listed with `-k`, displayed in reports, etc.
class Brakeman::NameOfCheck < Brakeman::BaseCheck
Brakeman::Checks.add self # This is how Brakeman finds all the checks
@description = "" # This description is only shown with `-k`
# This is the entry point for the check.
# Brakeman will call this method automatically when running
# all the checks.
def run_check
end
end
The name of the file can be anything with an .rb
extension, but traditionally the files are named check_name_of_check.rb
.
The heart of most Brakeman checks is a search for particular method calls.
To do this, use the tracker.find_call
utility.
For example, to find all calls to x.y
:
tracker.find_call(target: :x, method: :y)
This returns an array of results. Results are a hash that looks like this:
{
:target => :x, # Symbol representing the receiver of the method call
:method => :y, # Symbol representing the name of the method
:call => s(:call, s(:lvar, :x), :y), # Actual s-expression of the call
:nested => false, # Whether this call is actually the receiver of another call
:chain => [:x, :y],
:location => ... # Information about where the call is located
}
Note! By default, only non-nested calls are returned. In other words, the search above would not return x.y.z
or w.x.y
.
To also return nested calls (x.y.z
), pass in nested: true
.
It is also possible to search for many targets and methods at the same time:
tracker.find_call(target: [:x, :W], method: [:y, :z])
Brakeman's built-in notion of "dangerous" values are essentially Rails request values (query parameters, cookies, headers) and the database (Rails models).
Most checks use has_immediate_user_input? some_sexp
to check for dangerous values.
has_immediate_user_input?
returns either false
or a Match
struct. A Match
just has the type
of match (:params
, :cookies
, or :request
) and the value
itself that was matched.
The result of has_immediate_user_input?
is often passed to warn
when generating a warning.
To find uses of models, use has_immediate_model?
.
If you want to find user input (or model attributes) anywhere in a value, you can use include_user_input?
.
Typically these methods are called on the arguments of a "dangerous" method call.
Warnings are created via the warn
method.
This method takes a lot of options, but these are the most common:
warn result: ..., # The result hash
warning_type: "...", # Category of the warning
warning_code: :..., # The more specific warning type, which gets mapped to an integer
message: msg("..."), # Message displayed for the warning
confidence: ..., # Confidence level (:high, :medium, :weak)
user_input: ... # The s-expression to highlight as dangerous
It is best to pass in the :result
hash, because then Brakeman can pull out the code, file, line, etc. for the warning and they do not have to be specified explicitly.
This is a string representing the category of the warning. For example, "Cross-Site Scripting"
or "SQL Injection"
. In general, these strings can be anything. However, they are used to automatically link to documentation on https://brakemanscanner.org/docs/warning_types/, so it's recommended to use an existing category or manually set a link to your own documentation.
Brakeman has a set of "warning codes" which allow associating a warning with an unchanging integer value representing a slightly-more-specific warning "type". These codes are defined in WarningCodes
.
For custom rules, it is recommended to use :custom_check
as the :warning_code
.
Warning messages may set to a simple string, bu they are actually more flexible than that.
To create a message object, start with a call to msg(...)
.
The arguments to msg
can be strings or specific message types:
msg_code("...") # Format as a code snippet
msg_cve("CVE-...") # Format as a CVE number (may be linked to https://cve.mitre.org)
msg_input(match) # Change from :parameters or Match type to friendly words like "parameter value"
msg_file("...") # Format as a file name
msg_lit("...") # Do not format at all
msg_version(version, lib_name) # Format as a library version (defaults to "Rails", library name is optional)
Using these formatting options helps with consistency as well as enabling future translations.
In Brakeman, "confidence" is a bit of a conflation between "confidence this is a real problem" and "severity of the potential problem."
The possible values are :high
, :medium
, or :weak
.
This is the "dangerous value" to be highlighted in reports. Often this is a Match
value.
The relative path to the file where the warning exists.
Usually this does not need to be explicitly set.
The line number of the warning.
Usually this does not need to be explicitly set.
The relevant code (as an s-expression).
Often this does not need to be explicitly set.
A URL to more information regarding the warning category.
By default, this will be a page of https://brakemanscanner.org, but it may be set to anything.
The link is used in the JSON, HTML, and Markdown reports.
Almost all checks will want to avoid duplicate warnings.
In particular, this kind of thing causes problems in Brakeman:
x = system("ls #{params[:x]}") # Command injection
puts x # Command injection again, because value of x is used here
This is silly and confusing, so most checks will want a guard like this:
return unless original? result
This ensures the result
value is the original and not a dataflow copy.
Some checks might want the copy - cross-site scripting for example.
In that case, just do a duplicate check:
return if duplicate? result
add_result result
Custom checks can be loaded with the following command line option:
--add-checks-path path/to/checks/
Multiple paths can be specified, separated by commas.
Note that loading checks means running arbitrary Ruby code.
All checks include the Util
module.
This includes a number of helper methods, in particular ones for checking/accessing s-expressions, such as array?
, string?
, params?
, hash_access
, hash_iterate
, etc.
The Sexp
class also includes a number of helper methods, which are preferred over accessing node values directly. Besides being easier to read, the helper methods check that the type of the node matches the requested value. For example, you may only call Sexp#target
on an Sexp
with the type :call
.
Most Brakeman checks are run by default. However, it is possible to have a check off by default.
Change
Brakeman::Checks.add self
to
Brakeman::Checks.add_optional self