Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicitly handle each node type #178

Merged
merged 25 commits into from
Nov 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ func TestConcatenatingTaintedAndNonTaintedStrings(prefix string) {
s := core.Source{Data: "password1234"}
message := fmt.Sprintf("source: %v", s)
fullMessage := prefix + message
// TODO: no report should be produced for "prefix"
core.Sink(prefix) // want "a source has reached a sink"
core.Sink(prefix)
core.Sink(message) // want "a source has reached a sink"
core.Sink(fullMessage) // want "a source has reached a sink"
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ func TestRangeOverArray() {
sources := [1]core.Source{core.Source{Data: "password1234"}}
for i, s := range sources {
core.Sink(s) // want "a source has reached a sink"
// TODO want no diagnostic reported for string value
core.Sink(i) // want "a source has reached a sink"
core.Sink(i)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,30 +61,26 @@ func TestDeletingFromTaintedMapDoesNotTaintTheKey(key string, sources map[string

func TestMapUpdateWithTaintedValueDoesNotTaintTheKey(key string, value core.Source, sources map[string]core.Source) {
sources[key] = value
// TODO: no report should be produced here
core.Sink(key) // want "a source has reached a sink"
core.Sink(key)
}

func TestMapUpdateWithTaintedKeyDoesNotTaintTheValue(key core.Source, value string, sources map[core.Source]string) {
sources[key] = value
// TODO: no report should be produced here
core.Sink(value) // want "a source has reached a sink"
core.Sink(value)
}

func TestRangeOverMapWithSourceAsValue() {
m := map[string]core.Source{"secret": core.Source{Data: "password1234"}}
for k, s := range m {
core.Sink(s) // want "a source has reached a sink"
// TODO want no diagnostic reported for string key
core.Sink(k) // want "a source has reached a sink"
core.Sink(k)
}
}

func TestRangeOverMapWithSourceAsKey() {
m := map[core.Source]string{core.Source{Data: "password1234"}: "don't sink me"}
for src, str := range m {
core.Sink(src) // want "a source has reached a sink"
// TODO want no diagnostic reported for string value
core.Sink(str) // want "a source has reached a sink"
core.Sink(str)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@ func TestRangeOverSlice() {
sources := []core.Source{core.Source{Data: "password1234"}}
for i, s := range sources {
core.Sink(s) // want "a source has reached a sink"
// TODO want no diagnostic reported for string value
core.Sink(i) // want "a source has reached a sink"
core.Sink(i)
}
}

func TestRangeOverInterfaceSlice() {
for i, s := range []interface{}{core.Source{Data: "password1235"}} {
core.Sink(s) // want "a source has reached a sink"
// TODO want no diagnostic reported for string value
core.Sink(i) // want "a source has reached a sink"
core.Sink(i)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,8 @@ func TestPhiNodeDoesntPropagateTaintToOperands(i *core.Innocuous) {
if true {
ss = ii
}
// TODO: no report should be produced for ii
core.Sink(ii) // want "a source has reached a sink"
// TODO: no report should be produced for i
core.Sink(i) // want "a source has reached a sink"
core.Sink(ii)
core.Sink(i)
core.Sink(ss) // want "a source has reached a sink"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,5 @@ func TestSourcePointerAssertedFromParameterEface(e interface{}) {
func TestSourcePointerAssertedFromParameterEfaceCommaOk(e interface{}) {
s, ok := e.(*core.Source)
core.Sink(s) // want "a source has reached a sink"
_ = ok
core.Sink(ok)
}
244 changes: 142 additions & 102 deletions internal/pkg/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,104 +75,185 @@ func New(in ssa.Node, config classifier) *Source {
marked: make(map[ssa.Node]bool),
config: config,
}
s.dfs(in, map[*ssa.BasicBlock]int{}, nil)
s.dfs(in, map[*ssa.BasicBlock]int{}, nil, false)
return s
}

// dfs performs Depth-First-Search on the def-use graph of the input Source.
// While traversing the graph we also look for potential sanitizers of this Source.
// If the Source passes through a sanitizer, dfs does not continue through that Node.
func (s *Source) dfs(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
if s.marked[n] {
return
}
// booleans can't meaningfully be tainted
if isBoolean(n) {
func (s *Source) dfs(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock, isReferrer bool) {
Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function needed to be modified because the check for a call that is a referrer used to be done in visitReferrers. Since Call is the only special case, it feels cleaner to do it here. Other changes fell out of that change, because this function became unwieldy.

if s.shouldNotVisit(n, maxInstrReached, lastBlockVisited, isReferrer) {
return
}
s.preOrder = append(s.preOrder, n)
s.marked[n] = true

mirCopy := map[*ssa.BasicBlock]int{}
for m, i := range maxInstrReached {
mirCopy[m] = i
}

if instr, ok := n.(ssa.Instruction); ok {
// If the referrer is in a different block from the one we last visited,
// and it can't be reached from the block we are visiting, then stop visiting.
if ib := instr.Block(); lastBlockVisited != nil &&
ib != lastBlockVisited &&
!s.canReach(lastBlockVisited, ib) {
instrIndex, ok := indexInBlock(instr)
if !ok {
return
}

b := instr.Block()
if i, ok := indexInBlock(instr); ok && mirCopy[b] < i {
mirCopy[b] = i
if mirCopy[instr.Block()] < instrIndex {
mirCopy[instr.Block()] = instrIndex
}
lastBlockVisited = b
}
s.preOrder = append(s.preOrder, n)
s.marked[n] = true

s.visitReferrers(n, mirCopy, lastBlockVisited)
lastBlockVisited = instr.Block()
}

s.visitOperands(n, n.Operands(nil), mirCopy, lastBlockVisited)
s.visit(n, mirCopy, lastBlockVisited)
}

func (s *Source) visitReferrers(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
referrers := s.referrersToVisit(n, maxInstrReached)
func (s *Source) shouldNotVisit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock, isReferrer bool) bool {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: Consider writing for the positive rather than the negative, i.e. shouldVisit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I would agree, but in this case the function is only used in the negative case, so if it were named shouldVisit we would need to write !shouldVisit in the calling code, which is just as readable as shouldNotVisit, but feels a bit backwards to me: we're asking if we should visit, but we're actually interested in knowing if we shouldn't visit, so we immediately flip the result of the call.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, it's about reading the function while you're in it. While I'm looking at this function, I don't necessarily know all of its use-cases. I also don't know that consumption won't change to invert this into a double negative. At least, that's where I'm coming from, philosophically.

Then again, Naming is Hard:tm: and it's a bikeshed. It's fine as it is.

if s.marked[n] {
return true
}

for _, r := range referrers {
switch v := r.(type) {
case *ssa.Call:
if callee := v.Call.StaticCallee(); callee != nil && s.config.IsSanitizer(utils.DecomposeFunction(callee)) {
s.sanitizers = append(s.sanitizers, &sanitizer.Sanitizer{Call: v})
}
// booleans can't meaningfully be tainted
if isBoolean(n) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string notwithstanding, do any of the basic types meaningfully carry taint? Certainly string as a convertable type to []byte or []rune could carry taint, but does an individual byte or rune meaningfully carry taint?

If we're excluding basic types, we should consider excluding all numeric and pointer types.

Conversely, if memory serves, this came from the special handling surrounding val, ok := SomeExtract. If that's now / going to be being properly handled in the big instruction switch, then we shouldn't need to handle that here.

Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit pathological, but if each byte/rune from a tainted []byte or []rune were to be sinked individually, avoiding bytes and runes would miss that. (e.g. something like for _, r := range "secret" { fmt.Printf(r) }; again, this is kind of pathological). A similar thing could occur with some numeric types, e.g. in theory a cryptographic key could be sinked byte-by-byte.

we should consider excluding ... pointer types.

I think that could cause us to miss a lot of things. We don't know what a sink is going to do with a pointer argument, e.g. dereference it.

Conversely, if memory serves, ...

You are correct, but we also wanted to avoid things like fmt.Println(isSource(Source{})), so I think avoiding all booleans is justified.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could cause us to miss a lot of things. We don't know what a sink is going to do with a pointer argument, e.g. dereference it.

I was imprecise. I meant within the context of basic types - specifically, the types.BasicKinds, which fall into:

  • Invalid
  • Strings
  • Numeric types, including boolean
  • Unsafe pointers
  • Untyped types
  • The Byte and Rune alias for Uint8 and Int32 respectively.

So when I said "pointers," I should have said "unsafe pointers." I feel like unsafe, like reflect, is out of scope for contextual analysis for us.

So of the above, I would consider taint as able to propagate to strings (typed and untyped), and optionally runes (typed and untypted) and bytes.

You could make a similar pathological argument that someone could coerce a string -> []rune -> []int32 -> []int64 and back, but that feels like the sort of thing that is hard to avoid criticism in code review. \shrug

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this is another thing that falls into "could easily be its own PR." I was really just stirring conversation while it was front of mind, but we should shift this conversation to in Issue to not block this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Issue here: #191

return true
}

if instr, ok := n.(ssa.Instruction); ok {
Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm doing a second if that has the same head as the previous one because I wanted to detangle two things:

  1. should we keep traversing?
  2. update the max instruction in block, if applicable

These should really be replaced by calls to dedicated functions. I'm happy to do it in this PR, but I could also do it in a future PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and extracted the first if (as well as other related instructions) into its own function. I think this reads much better.

instrIndex, ok := indexInBlock(instr)
if !ok {
return true
}

// If the referrer is in a different block from the one we last visited,
// and it can't be reached from the block we are visiting, then stop visiting.
if lastBlockVisited != nil && instr.Block() != lastBlockVisited && !s.canReach(lastBlockVisited, instr.Block()) {
return true
}

s.dfs(r.(ssa.Node), maxInstrReached, lastBlockVisited)
// If this call's index is lower than the highest seen so far in its block,
// then this call is "in the past". If this call is a referrer,
// then we would be propagating taint backwards in time, so stop traversing.
// (If the call is an operand, then it is being used as a value, so it does
// not matter when the call occurred.)
if _, ok := instr.(*ssa.Call); ok && instrIndex < maxInstrReached[instr.Block()] && isReferrer {
return true
}
}

return false
}

// referrersToVisit produces a filtered list of Referrers for an ssa.Node.
// Specifically, we want to avoid referrers that shouldn't be visited, e.g.
// because they would not be reachable in an actual execution of the program.
func (s *Source) referrersToVisit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int) (referrers []ssa.Instruction) {
if n.Referrers() == nil {
func (s *Source) visit(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
if s.node == n {
s.visitReferrers(n, maxInstrReached, lastBlockVisited)
return
}
for _, r := range *n.Referrers() {
if c, ok := r.(*ssa.Call); ok {
// This is to avoid attaching calls where the source is the receiver, ex:
// core.Sinkf("Source id: %v", wrapper.Source.GetID())
if recv := c.Call.Signature().Recv(); recv != nil && s.config.IsSourceType(utils.DecomposeType(utils.Dereference(recv.Type()))) {
continue
}

// If this call's index is lower than the highest in its block,
// then this call is "in the past" and we should stop traversing.
i, ok := indexInBlock(r)
if !ok {
continue
}
if i < maxInstrReached[r.Block()] {
continue
}
switch t := n.(type) {
case *ssa.Alloc:
// An Alloc represents the allocation of space for a variable. If a Node is an Alloc,
// and the thing being allocated is not an array, then either:
// a) it is a Source value, in which case it will get its own traversal when sourcesFromBlocks
// finds this Alloc
// b) it is not a Source value, in which case we should not visit it.
// However, if the Alloc is an array, then that means the source that we are visiting from
// is being placed into an array, slice or varags, so we do need to keep visiting.
if _, isArray := utils.Dereference(t.Type()).(*types.Array); isArray {
s.visitReferrers(n, maxInstrReached, lastBlockVisited)
}

if fa, ok := r.(*ssa.FieldAddr); ok {
deref := utils.Dereference(fa.X.Type())
typPath, typName := utils.DecomposeType(deref)
fieldName := utils.FieldName(fa)
case *ssa.Call:
if callee := t.Call.StaticCallee(); callee != nil && s.config.IsSanitizer(utils.DecomposeFunction(callee)) {
s.sanitizers = append(s.sanitizers, &sanitizer.Sanitizer{Call: t})
}

if !s.config.IsSourceField(typPath, typName, fieldName) {
continue
}
// This is to avoid attaching calls where the source is the receiver, ex:
// core.Sinkf("Source id: %v", wrapper.Source.GetID())
if recv := t.Call.Signature().Recv(); recv != nil && s.config.IsSourceType(utils.DecomposeType(utils.Dereference(recv.Type()))) {
return
}
Comment on lines +174 to +176
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this not also exclude, e.g., wrapper.Source.GetSecret()?

Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. This is actually one of the more significant architectural flaws in the codebase right now, IMO.

The reason is that GetSecret will be identified as a fieldpropagator, which will be found in levee when checking for Calls, which will start a new traversal with the result of GetSecret as a source. I think this is unintuitive to say the least. I had a PR for using fieldpropagator in source a long time ago, #77, but I think I ended up closing it because I wanted the fieldpropagators to be consumed by sourcetype or something of the sort.

As a side note, currently the propagation logic in fieldpropagator is not nearly as developed as that in source, and in fact I believe that logic should probably be shared: #130.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. And yeah, you know opinion on the pipeline of analyzers here. If these were unified in levee instead of propagation happening all in source... but that's a conversation for a future PR.


s.visitReferrers(n, maxInstrReached, lastBlockVisited)
s.visitOperands(n, maxInstrReached, lastBlockVisited)

case *ssa.FieldAddr:
deref := utils.Dereference(t.X.Type())
typPath, typName := utils.DecomposeType(deref)
fieldName := utils.FieldName(t)
if !s.config.IsSourceField(typPath, typName, fieldName) {
return
}
s.visitReferrers(n, maxInstrReached, lastBlockVisited)
s.visitOperands(n, maxInstrReached, lastBlockVisited)

// Everything but the actual integer Index should be visited.
case *ssa.Index:
s.visitReferrers(n, maxInstrReached, lastBlockVisited)
s.dfs(t.X.(ssa.Node), maxInstrReached, lastBlockVisited, false)

// The actual integer Index should not be visited.
// Everything but the actual integer Index should be visited.
case *ssa.IndexAddr:
s.visitReferrers(n, maxInstrReached, lastBlockVisited)
s.dfs(t.X.(ssa.Node), maxInstrReached, lastBlockVisited, false)

// Only the Map itself can be tainted by an Update.
// The Key can't be tainted.
// The Value can propagate taint to the Map, but not receive it.
// MapUpdate has no referrers, it is only an Instruction, not a Value.
Comment on lines +202 to +205
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Certainly out of scope for this PR, possibly out of scope for this analyzer, but do we need to consider where keys are tainted? E.g.

func TestFoo() {
	s := core.Source{Data: "password1234"}
	m := map[core.Source]string{s: s.Data}
	for src, str := range m {
		core.Sink(src) // want "a source has reached a sink"
		core.Sink(str) // want "a source has reached a sink"
	}
}

Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. With this case in mind, I think in general we may want to consider anything coming out of a tainted map to be tainted. The current tests don't do that, e.g.

func TestRangeOverMapWithSourceAsKey() {
	m := map[core.Source]string{core.Source{Data: "password1234"}: "don't sink me"}
	for src, str := range m {
		core.Sink(src) // want "a source has reached a sink"
		core.Sink(str)
	}
}

In the above cases, the fix would be to propagate through the Next instruction. In the current PR, we do not propagate through those instructions at all; on master, we do. I think for now it would be best to add that case you mentioned and to keep propagating through Next instructions. WDYT?

(For MapUpdate though I'm confident we don't want to traverse to anything other than the Map).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I'm going to use sources in keys and strings in values in this comment. The same logic applies in reverse.)

I think if we propagate through Next, we might end up tainting keys categorically where we shouldn't. It could be worth a shot and seeing how it plays out though.

But the more I think about it, the more I think this might be out of scope. It feels like we're needing to decide categorically whether or not both keys and values are tainted if a map contains a source type value. Otherwise, we would have to make a distinction between the map "safely" holding its values versus having been tainted by receiving a tainted value as a key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG. I've added it to #188.

case *ssa.MapUpdate:
s.dfs(t.Map.(ssa.Node), maxInstrReached, lastBlockVisited, false)

// The only Operand that can be tainted by a Send is the Chan.
// The Value can propagate taint to the Chan, but not receive it.
// Send has no referrers, it is only an Instruction, not a Value.
case *ssa.Send:
s.dfs(t.Chan.(ssa.Node), maxInstrReached, lastBlockVisited, false)

// These nodes' operands should not be visited, because they can only receive
// taint from their operands, not propagate taint to them.
case *ssa.BinOp, *ssa.ChangeInterface, *ssa.ChangeType, *ssa.Convert, *ssa.Extract, *ssa.MakeChan, *ssa.MakeMap, *ssa.MakeSlice, *ssa.Phi, *ssa.Range, *ssa.Slice, *ssa.UnOp:
s.visitReferrers(n, maxInstrReached, lastBlockVisited)

// These nodes don't have operands; they are Values, not Instructions.
case *ssa.Const, *ssa.FreeVar, *ssa.Global, *ssa.Lookup, *ssa.Parameter:
s.visitReferrers(n, maxInstrReached, lastBlockVisited)

// These nodes don't have referrers; they are Instructions, not Values.
case *ssa.Go, *ssa.Store:
s.visitOperands(n, maxInstrReached, lastBlockVisited)

// These nodes are both Instructions and Values, and currently have no special restrictions.
case *ssa.Field, *ssa.MakeInterface, *ssa.Select, *ssa.TypeAssert:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should *ssa.Field have the same special handling that ssa.FieldAddr has?

Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think almost certainly, yes. See #188. TLDR: since we don't have a good suite of tests for Field, I think for now it would be safer to keep the existing behavior of traversing to everything.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to an above comment, but I might expect the special handling of not propagating through the CommaOK value of a type assert to be handled here - at least if we weren't otherwise claiming that a boolean can't be tainted.

Copy link
Contributor Author

@mlevesquedion mlevesquedion Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do that by checking if the TypeAssert is CommaOk, and if so, avoiding the 1st Referrer. I think it's simpler to rely on the code for avoiding booleans, though.

Looking into this, it looks like the CommaOk case for type asserts was not covered, so I added a line to an existing test:

func TestSourcePointerAssertedFromParameterEfaceCommaOk(e interface{}) {
	s, ok := e.(*core.Source)
	core.Sink(s) // want "a source has reached a sink"
	core.Sink(ok) <-- this is the line I added, no report should be produced here
}

s.visitReferrers(n, maxInstrReached, lastBlockVisited)
s.visitOperands(n, maxInstrReached, lastBlockVisited)

// These nodes cannot propagate taint.
case *ssa.Builtin, *ssa.DebugRef, *ssa.Defer, *ssa.Function, *ssa.If, *ssa.Jump, *ssa.MakeClosure, *ssa.Next, *ssa.Panic, *ssa.Return, *ssa.RunDefers:

default:
fmt.Printf("unexpected node received: %T %v; please report this issue\n", n, n)
}
}

func (s *Source) visitReferrers(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
if n.Referrers() == nil {
return
}
for _, r := range *n.Referrers() {
s.dfs(r.(ssa.Node), maxInstrReached, lastBlockVisited, true)
}
}

referrers = append(referrers, r)
func (s *Source) visitOperands(n ssa.Node, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
for _, o := range n.Operands(nil) {
if *o == nil {
continue
}
s.dfs((*o).(ssa.Node), maxInstrReached, lastBlockVisited, false)
}
return referrers
}

func (s *Source) canReach(start *ssa.BasicBlock, dest *ssa.BasicBlock) bool {
Expand All @@ -198,47 +279,6 @@ func (s *Source) canReach(start *ssa.BasicBlock, dest *ssa.BasicBlock) bool {
return false
}

func (s *Source) visitOperands(from ssa.Node, operands []*ssa.Value, maxInstrReached map[*ssa.BasicBlock]int, lastBlockVisited *ssa.BasicBlock) {
// Do not visit Operands if the current node is an Extract.
// This is to avoid incorrectly tainting non-Source values that are
// produced by an Instruction that has a Source among the values it
// produces, e.g. a call to a function with a signature like:
// func NewSource() (*core.Source, error)
// Which leads to a flow like:
// Extract (*core.Source) --> Call (NewSource) --> error
if _, ok := from.(*ssa.Extract); ok {
return
}

for _, o := range operands {
n, ok := (*o).(ssa.Node)
if !ok {
continue
}

// An Alloc represents the allocation of space for a variable. If a Node is an Alloc,
// and the thing being allocated is not an array, then either:
// a) it is a Source value, in which case it will get its own traversal when sourcesFromBlocks
// finds this Alloc
// b) it is not a Source value, in which case we should not visit it.
// However, if the Alloc is an array, then that means the source that we are visiting from
// is being placed into an array, slice or varags, so we do need to keep visiting.
if al, isAlloc := (*o).(*ssa.Alloc); isAlloc {
if _, isArray := utils.Dereference(al.Type()).(*types.Array); !isArray {
continue
}
}

// Don't traverse to the key in a lookup.
// For example, if a map is tainted, looking up a value in the map
// doesn't taint the key, so we shouldn't traverse to the key.
if look, ok := from.(*ssa.Lookup); ok && *o == look.Index {
continue
}
s.dfs(n, maxInstrReached, lastBlockVisited)
}
}

// compress removes the elements from the graph that are not required by the
// taint-propagation analysis. Concretely, only propagators, sanitizers and
// sinks should constitute the output. Since, we already know what the source
Expand Down Expand Up @@ -374,7 +414,7 @@ func sourcesFromBlocks(fn *ssa.Function, conf classifier) []*Source {
}

// source obtained through a field or an index operation
case *ssa.Field, *ssa.FieldAddr, *ssa.IndexAddr, *ssa.Lookup:
case *ssa.Field, *ssa.FieldAddr, *ssa.Index, *ssa.IndexAddr, *ssa.Lookup:

// source chan or map (arrays and slices have regular Allocs)
case *ssa.MakeMap, *ssa.MakeChan:
Expand Down