Upload
devseccon
View
108
Download
0
Embed Size (px)
Citation preview
Join the conversation #devseccon
By Justin Collins (@presidentbeef)
Practical Static Analysis for Continuous Application Security
Continuous Security
Automated, always-on processes for detecting potential security vulnerabilities
5
1 - Identify a Problem
Repeated incidents with the same root causeOpt-in security, e.g. in APIsUnsafe calls/settings no one should use, ever
11
2 - Identify a Solution
Write a safer libraryMake security opt-outDetect unsafe calls/settings/API usage
12
4 - Automate Enforcement
Part of continuous integrationPart of code reviewStandalone, continuously-running processLocal scripts/hooks
14
@presidentbeef
devseccon2016github.com/presidentbeef/devseccon
22
Bash
git checkout $@ --quiet
files=$(git diff --name-status master HEAD | grep -E "^(A|M)" | cut -f 2)
grep "delete_survey([^,)]\+)" $files
30
git diff --name-status
A README.md
M lib/blah.py
M lib/a.rb
M lib/version.rb
D test/new_stuff.yml
31
Bash
git checkout $@ --quiet
files=$(git diff --name-status master HEAD | grep -E "^(A|M)" | cut -f 2)
grep "delete_survey([^,)]\+)" $files
32
Create a Rule
class CheckSurvey < Rule def run(file_name, code) if code =~ /delete_survey\([^,)]+\)/ warn file_name, "delete_survey without user ID" end endend
35
Base Rule Classclass Rule @rules = [] @warnings = []
def self.inherited klass @rules << klass end
def self.run_rules files files.each do |name|
code = File.read(name)
@rules.each do |r| r.new.run(name, code) end end end
def self.warnings @warnings end
def warn file_name, msg Rule.warnings << [file_name, msg] endend 36
Code to Run It
system "git checkout #{ARGV[0]}"
files = `git diff --name-status master HEAD | grep -E "^(A|M)" | cut -f 2`
Rule.run_rules(files.split)
Rule.warnings.each do |file_name, message| puts "#{message} in #{file_name}"end
if Rule.warnings.any? exit 1end
37
False Positives
# Remember not to use delete_survey(survey_id)!
function delete_survey(node) {...}
39
Ruby (RubyParser)
s(:call, nil, :delete_survey, s(:call, nil, :survey_id))
RubyParser.new.parse("delete_survey(survey_id")
44
Python (Astroid)
Module()
body = [
Discard()
value =
CallFunc()
func =
Name(delete_survey)
args = [
Name(survey_id)
]
starargs =
kwargs =
]
AstroidBuilder().string_build("delete_survey(survey_id)")
45
JavaScript (Esprima)
{ "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "delete_survey" }, "arguments": [ { "type": "Identifier", "name": "survey_id" } ] } } ], "sourceType": "script"}
esprima.parse("delete_survey(survey_id)")
46
Bandit Custom Ruleimport bandit
from bandit.core import test_properties as test
@test.checks('Call')
@test.test_id('B350')
def unsafe_get_survey(context):
if (context.call_function_name_qual == 'delete_survey' and
context.call_args_count < 2):
return bandit.Issue(
severity=bandit.HIGH,
confidence=bandit.HIGH,
text="Use of delete_survey without user ID."
) 49
Brakeman Custom Checkrequire 'brakeman/checks/base_check'
class Brakeman::CheckDeleteSurvey < Brakeman::BaseCheck Brakeman::Checks.add self Brakeman::WarningCodes::Codes[:get_survey] = 2001
@description = "Finds delete_survey calls without a user_id"
def run_check @tracker.find_call(target: false, method: :delete_survey).each do |result| next if duplicate? result
add_result result
if result[:call].second_arg.nil? warn :result => result, :warning_type => "Direct Object Reference", :warning_code => :get_survey, :message => "Use of get_survey without user_id", :confidence => CONFIDENCE[:high] end end endend 52
JavaScript (Esprima)
{ "type": "Program", "body": [ { "type": "ExpressionStatement", "expression": { "type": "CallExpression", "callee": { "type": "Identifier", "name": "delete_survey" }, "arguments": [ { "type": "Identifier", "name": "survey_id" } ] } } ], "sourceType": "script"}
esprima.parse("delete_survey(survey_id)")
57
Walking Esprima ASTvar check_get_survey = function(ast) { if(ast.type == "CallExpression" && ast.callee.type == "Identifier" && ast.callee.name == "delete_survey" && ast.arguments.length == 1) {
console.log("delete_survey called without user_id at line ",
ast.loc.start) }}
var walk = function(ast) { if(Array.isArray(ast)) { ast.forEach(walk); } else if(ast.type) { check_get_survey(ast)
for (key in ast) { walk(ast[key]) } }}
var esprima = require('esprima');walk(esprima.parse('delete_survey(survey_id)', { loc: true })) 58
Ruby (RubyParser)
s(:call, nil, :delete_survey, s(:call, nil, :survey_id))
RubyParser.new.parse("get_survey(survey_id")
60
Walking RubyParser AST
require 'ruby_parser'
require 'sexp_processor'
class FindGetSurvey < SexpInterpreter def process_call exp if exp[1].nil? and exp[2] == :delete_survey and exp[4].nil?
puts "delete_survey called without user_id at line #{exp.line}" end endend
ast = RubyParser.new.parse "delete_survey(survey_id)"FindGetSurvey.new.process ast
61
Summary
Start small: identify single issue to solveTailor solution to your environmentAutomate enforcement
62