Skip to content

Creating Custom Brakeman Rules

Justin edited this page Oct 8, 2019 · 3 revisions

Overview

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.

Writing a Check

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.

Finding Calls

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])

Checking for "Dangerous" Values

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.

Generating a Warning

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

Warning Options

result

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.

warning_type

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.

warning_code

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.

message

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.

confidence

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.

user_input

This is the "dangerous value" to be highlighted in reports. Often this is a Match value.

file

The relative path to the file where the warning exists.

Usually this does not need to be explicitly set.

line

The line number of the warning.

Usually this does not need to be explicitly set.

code

The relevant code (as an s-expression).

Often this does not need to be explicitly set.

link

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.

Avoiding Duplicates

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

Loading Custom Checks

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.

Miscellaneous

Utilities

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.

Optional Checks

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