63
Join the conversation #devseccon By Justin Collins (@presidentbeef) Practical Static Analysis for Continuous Application Security

Justin collins - Practical Static Analysis for continuous application delivery

Embed Size (px)

Citation preview

Join the conversation #devseccon

By Justin Collins (@presidentbeef)

Practical Static Analysis for Continuous Application Security

@presidentbeef

Background

2

@presidentbeef

Static analysis security tool

for Ruby on Rails3

@presidentbeef

Definitions

4

Continuous Security

Automated, always-on processes for detecting potential security vulnerabilities

5

Static Analysis

Information determined about a program

without actually running the code

6

Practical Static Analysis

Things you can reasonably implement after this workshop

7

Why Static Analysis?

FastRun anytimeEasy to automatePoints directly at suspect code

8

Tool Cycle

Run Tool Wait Interpret Results Fix Issues

Repeat

9

@presidentbeef

Getting Started

10

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

3 - Enforce the Solution

Write testsUse static analysisUse dynamic analysis

13

4 - Automate Enforcement

Part of continuous integrationPart of code reviewStandalone, continuously-running processLocal scripts/hooks

14

@presidentbeef

Automation Strategies

15

Continuous Integration

16

Code Review

17

Deployment Gate

18

Separate Process

19

Local Tests/Git Hook

20

@presidentbeef

Scenario

21

@presidentbeef

ssh [email protected]

devseccon2016github.com/presidentbeef/devseccon

22

1 - Identify a Problem

delete_survey(survey_id)

23

2 - Identify a Solution

delete_survey(survey_id, user_id)

24

3 - Enforce the Solution

Use static analysis to check for use

25

@presidentbeef

Statically Analyzing

26

Regular Expressions

• grep• ack• ag• …or build your own

27

grep/ack

grep -R "delete_survey([^,)]\+)" *

ack "delete_survey\([^,)]+\)" --type=python

28

Changed Files

grep

file1file2file3...

Desired Flow

29

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

Bash

bash check_stuff.sh 9c2ca25

33

Changed Files

Rules

file1 - warning Xfile2 - warning Yfile3 - warning X...

Multiple Rules

34

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

Ruby

ruby check_delete.rb 9c2ca25

38

False Positives

# Remember not to use delete_survey(survey_id)!

function delete_survey(node) {...}

39

False Negatives

delete_survey(input.split(",")[0])

40

@presidentbeef

Abstract Syntax Trees

41

@presidentbeef

delete_survey(survey_id)

call

args

local

nil"delete_survey"

"survey_id"

42

S-Expressions

(call nil "delete_survey" (local "survey_id"))

delete_survey(survey_id)

43

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

@presidentbeef

Existing Tools

47

@presidentbeef

Bandit

48

https://github.com/openstack/bandit

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

Bandit Custom Warning

50

brakeman.org

51

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

Brakeman Custom Check

brakeman --add-checks-path custom_checks/

53

Brakeman Custom Warning

54

@presidentbeef

Custom Tools

55

@presidentbeef

Esprima

esprima.org

56

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

@presidentbeef

RubyParser

github.com/seattlerb/ruby_parser

59

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

Join the conversation #devseccon

Thank You

Source Code:https://github.com/presidentbeef/devseccon