Skip to content
This repository has been archived by the owner on Jan 5, 2023. It is now read-only.

Protobuf modelling #285

Merged
merged 27 commits into from
Sep 7, 2020
Merged

Protobuf modelling #285

merged 27 commits into from
Sep 7, 2020

Conversation

smowton
Copy link
Contributor

@smowton smowton commented Aug 11, 2020

This isn't quite done yet, but it's close enough that I'd appreciate feedback on the trickier points here.

Problems to overcome:

  • Desire to taint the output of Marshal based on transitive field / array-element writes (e.g. msg.SubMessage.RepeatedMessage[2].Description = somethingNasty())
  • Desire to propagate taint properly across Merge and Clone, which should copy taint from corresponding field to field
  • Desire to support MarshalOptions.MarshalState, which takes its message from and returns its buffer in a struct field

Current approach:

  • The read-side of MarshalOptions.MarshalState precisely models the field the taint is consumed from. The write-side simply taints the whole output, since it only has one field, and relies on the default taint rule that object taint implies field-read taint.
  • Messages are tainted as a whole, which simplifies Merge and Clone operations but complicates the additional steps needed to convey taint from a field-write to the underlying SSA variable (I extend SsaWithFields to navigate the access path consisting of field-accesses and element-reads, but this might not be appropriate and we might need a separate mechanism for this). This also could produce false-positives by spreading taint to untainted fields (and of course array elements, but this is more ordinary); whether this is a practical problem depends on whether people like to use their protobuf structs as working data structures, or if they simply populate them immediately before serialisation (i.e., if the fields are in practice write-only)

Possible changes:

  • Given I gave up on field-sensitive modelling of Marshal for the time being, we could in the same spirit throw away the complicated MarshalOptions.MarshalState stuff and simply propagate taint from a Message to a MarshalInput struct.
  • Extending SsaWithFields as I have done might be inappropriate because its similar() method will currently conflate access paths using differing indices. In that case we need a different way to walk down an access path consisting of field-read and element-read operations to taint the bottom Message in the pile.
  • If any of this is too contentious I could back off some of today's experiments and simply PR the easy partial support and follow up with either custom hackery as seen here, or with a more general mechanism to state a function model that deals in access paths rather than simply direct inputs and outputs.

@smowton smowton requested a review from a team August 11, 2020 17:31
@smowton
Copy link
Contributor Author

smowton commented Aug 12, 2020

I added further tests including ones exhibiting inaccuracy of the current implementation.

TODO:

  • Clean up the test directory, which contains many redundant copies of the protoc output and some debugging queries that don't need to be in the final PR
  • Add support for UnmarshalState, MergeState?
  • Change note

@smowton
Copy link
Contributor Author

smowton commented Aug 12, 2020

I also note a search of Github suggests this is now the only user of MarshalOptions.MarshalState that exists (the API was only introduced earlier this year)

@smowton
Copy link
Contributor Author

smowton commented Aug 12, 2020

TODOs done apart from the change-note, which I'll write last so as not to forget to update it.

I gave up on modelling UnmarshalOptions.UnmarshalState, as that would need to propagate taint like

func testUnmarshalOptions() {
	options := proto.UnmarshalOptions{}

	untrustedSerialized := getUntrustedBytes()
	query := &query.Query{}

	state := protoiface.UnmarshalInput {
		Message: query.ProtoReflect(),
		Buf: untrustedSerialized,
		Flags: 0,
		Resolver: nil,
	}

	options.UnmarshalState(state)
	
	sinkString(query.Description) // BAD
}

That would need us to work backwards from the input struct to its field, and then from .ProtoReflect() call to the query it actually refers to. Ultimately doable but painful.

@owen-mc
Copy link
Contributor

owen-mc commented Aug 12, 2020

I also note a search of Github suggests this is now the only user of MarshalOptions.MarshalState that exists (the API was only introduced earlier this year)

But note that github code search is pretty unreliable.

@smowton smowton changed the title Protobuf support (draft) Protobuf modelling Aug 13, 2020
Copy link
Collaborator

@max-schaefer max-schaefer left a comment

Choose a reason for hiding this comment

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

Partial review with a few simple suggestions; will continue later.

class SsaWithFields extends SsaWithFieldsAndElements {
SsaWithFields() {
this = TRoot(_) or
exists(SsaWithFieldsAndElements base | this = TFieldStep(base, _))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
exists(SsaWithFieldsAndElements base | this = TFieldStep(base, _))
exists(SsaWithFields base | this = TFieldStep(base, _))

}

/**
* Additional taint-flow step modelling flow from MarshalInput.Message to MarshalOutput,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* Additional taint-flow step modelling flow from MarshalInput.Message to MarshalOutput,
* Additional taint-flow step modelling flow from `MarshalInput.Message` to `MarshalOutput`,


/**
* Additional taint-flow step modelling flow from MarshalInput.Message to MarshalOutput,
* mediated by a MarshalOptions.MarshalState call.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* mediated by a MarshalOptions.MarshalState call.
* mediated by a `MarshalOptions.MarshalState` call.

* Additional taint-flow step modelling flow from MarshalInput.Message to MarshalOutput,
* mediated by a MarshalOptions.MarshalState call.
*
* Note we can taint the whole MarshalOutput as it only has one field (Buf), and taint-
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
* Note we can taint the whole MarshalOutput as it only has one field (Buf), and taint-
* Note we can taint the whole `MarshalOutput` as it only has one field (`Buf`), and taint-

Copy link
Contributor

Choose a reason for hiding this comment

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

In case you didn't know, you can click the + for comments and drag to make a multi-line comment or suggestion.

Comment on lines 70 to 71
passedMarshalInput.asExpr().getGlobalValueNumber() =
marshalInput.asExpr().getGlobalValueNumber() and
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
passedMarshalInput.asExpr().getGlobalValueNumber() =
marshalInput.asExpr().getGlobalValueNumber() and
globalValueNumber(passedMarshalInput) = globalValueNumber(marshalInput) and

Alternatively, you can replace the single use of passedMarshalInput with globalValueNumber(marshalInput).getANode().

Comment on lines 160 to 161
succ.(DataFlow::PostUpdateNode).getPreUpdateNode().asInstruction() =
accessPath.getBaseVariable().getAUse()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
succ.(DataFlow::PostUpdateNode).getPreUpdateNode().asInstruction() =
accessPath.getBaseVariable().getAUse()
succ = DataFlow::ssaNode(accessPath.getBaseVariable())

@max-schaefer
Copy link
Collaborator

max-schaefer commented Aug 14, 2020

Desire to propagate taint properly across Merge and Clone, which should copy taint from corresponding field to field

I seem to vaguely remember that we don't do field-sensitive propagation through taint, only through plain data flow. In other words, if we know that there is data flow between x and y, then we will conclude that the same is true for x.f and y.f. If, on the other hand, all we know is that taint is propagated from x to y, we don't do the same. Have you tried promoting your models of Merge and Clone from TaintTracking::FunctionModels to DataFlow::FunctionModels to see if that helps?

@smowton
Copy link
Contributor Author

smowton commented Aug 14, 2020

Distcompare result was unremarkable

Copy link
Contributor

@sauyon sauyon left a comment

Choose a reason for hiding this comment

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

My rather belated review. (Sorry!)

TFieldStep(SsaWithFieldsAndElements base, Field f) { exists(accessPathFieldAux(base, f)) } or
TElementStep(SsaWithFieldsAndElements base, IR::Instruction idx) {
exists(accessPathElementAux(base, idx))
}

/**
* Gets a representation of `nd` as an ssa-with-fields value if there is one.
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this comment was out of date, can you fix it?

/**
* Gets the SSA variable corresponding to the base of this SSA variable with fields.
* Gets the SSA variable corresponding to the base of this SSA variable with fields and/or array elements.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this not also include maps?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I forgot they were a first-class type. Looks like the Go implementation does indeed produce maps too -- I'll add this.

TRoot(SsaVariable v) or
TStep(SsaWithFields base, Field f) { exists(accessPathAux(base, f)) }
TFieldStep(SsaWithFieldsAndElements base, Field f) { exists(accessPathFieldAux(base, f)) } or
TElementStep(SsaWithFieldsAndElements base, IR::Instruction idx) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to use GVNs here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You mean as an alternative to IR::Instruction as the representation of the index? That does sound better, I was just worried that because GVN analysis is based on the SSA representation this would invert the pass order.

exists(Field f | this = TFieldStep(_, f) | result = f.getType())
or
exists(SsaWithFieldsAndElements base | this = TElementStep(base, _) |
result = base.getType().getUnderlyingType().(ArrayType).getElementType()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should also deal with slices and maps, as well as maybe strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Slices and maps agreed -- I'll do both.

* Additional taint-flow step modelling flow from MarshalInput.Message to MarshalOutput,
* mediated by a MarshalOptions.MarshalState call.
*
* Note we can taint the whole MarshalOutput as it only has one field (Buf), and taint-
Copy link
Contributor

Choose a reason for hiding this comment

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

In case you didn't know, you can click the + for comments and drag to make a multi-line comment or suggestion.

Comment on lines 65 to 72
// pred -> marshalInput.Message
any(DataFlow::Write w)
.writesField(marshalInput.(DataFlow::PostUpdateNode).getPreUpdateNode(),
inputMessageField(), pred) and
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make sense to just make any write to MarshalInput.Message taint the entire struct? I feel like that would reduce the complexity of this quite a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, this is probably overkill.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Remind me, have we checked that this is sensible in practice, i.e., if one field is tainted then so are the rest?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this case the fields are an incoming byte buffer, the message to be serialized into it, and some flags, so I don't think there's a strong chance of a false positive.

@@ -0,0 +1,129 @@
package main
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you make the tests use something like the comment tests from here and it's corresponding ql? I think it's something we should move to.

I'm also happy to do it myself if you'd like.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, sounds like a good step, I'll have a go

*/
private class WriteMessageFieldStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(SsaWithFieldsAndElements accessPath |
Copy link
Contributor

Choose a reason for hiding this comment

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

Though I think SsaWithFieldsAndElements might be useful elsewhere, I think it might also be useful to have a superclass for instructions / nodes / exprs that have a "base" so you can just use .getBase()+ here.

@smowton
Copy link
Contributor Author

smowton commented Aug 20, 2020

Pushed fixes for the simple review comments, will get to Sauyon's improvements this afternoon.

@smowton
Copy link
Contributor Author

smowton commented Aug 27, 2020

@max-schaefer @sauyon I've pushed the two big changes: support for maps, and using getBase() instead of SsaWithFieldsAndElements, as the top two commits. If you like the getBase solution better I'll drop the SsaWithFieldsAndElements commit.

@max-schaefer
Copy link
Collaborator

If you like the getBase solution better I'll drop the SsaWithFieldsAndElements commit.

Yes, I much prefer it. I hadn't realised (or perhaps had forgotten) that we weren't matching up the series of field and element accesses between write and read anyway, so there really isn't much point to capturing it in a special data structure.

Copy link
Collaborator

@max-schaefer max-schaefer left a comment

Choose a reason for hiding this comment

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

A few more suggestions. IIUC, moving away from SsaWithFieldsAndElements should simplify this considerably, which I look forward to.

/**
* A data-flow node that reads the value of a field from a struct, or an element from an array, slice, map or string.
*/
abstract class ReadFromAggregateNode extends ReadNode {
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍 to introducing this.

Inevitable naming bikeshed: I don't think "aggregate" is a common term in Go (it's not mentioned in the language spec), and of course in QL aggregates are something completely different. How about using "component" as an umbrella term for "element" and "field", and then renaming this class to ComponentReadNode?

Also, I would strongly advise not making this abstract but instead going the slightly more convoluted route of introducing corresponding IR instructions and AST node types and overriding the type of insn. This may seem like overkill, but you'll thank me eventually.

On a similar note, instead of making getBase() abstract (meaning that every subclass has to override it), implement it as none().

*
* For example, in the expression a.b[c].d[e], this would return the dataflow node for the read from `a`.
*/
Node getUnderlyingNode(ReadNode read) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

It seems like this is currently only used in Protobuf.qll, so perhaps just move it there?

ql/src/semmle/go/frameworks/Protobuf.qll Show resolved Hide resolved

private Field inputMessageField() {
result
.hasQualifiedName("google.golang.org/protobuf/runtime/protoiface", "MarshalInput", "Message")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would it make sense to pull out the very long name of this package into a helper predicate to avoid repetition and line wrapping?

Comment on lines 65 to 72
// pred -> marshalInput.Message
any(DataFlow::Write w)
.writesField(marshalInput.(DataFlow::PostUpdateNode).getPreUpdateNode(),
inputMessageField(), pred) and
Copy link
Collaborator

Choose a reason for hiding this comment

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

Remind me, have we checked that this is sensible in practice, i.e., if one field is tainted then so are the rest?

*/
private class WriteMessageFieldStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node pred, DataFlow::Node succ) {
exists(DataFlow::ReadNode base | succ = DataFlow::getUnderlyingNode(base) |
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this do the right thing? Shouldn't succ rather be the post-update node whose pre-update is the base of the write?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding the post-update node stuff loses you cases like x[0].y = value, because x does not have a post-update node. What do you recommend here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, hm, I see. But what's the use of having a step from value to x in this example? There isn't any outgoing flow from x, is there? So wouldn't the flow get stuck?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha, looks like I had a pair of nearly-cancelling bugs -- the default taint rule for derefs was conveying the taint from the operand of an implicit deref (here x) back out to the deref (implicit *x) and then to the post-update node of x -- which did exist because of the implicit deref operation.

Only for complex cases I was missing the post-update node's existence because I was failing to account for implicit derefs happening during x[0].y.

I've now fixed that omission and introduced a cast to PostUpdateNode as expected. This means that this case works, due to an implicit deref of x at the top level:

x := &SomeType{}
x.y[z].w[v].a = getTaint()

But this case doesn't, as x doesn't have a post-update node:

x := SomeType{}
x.y[z].w[v].a = getTaint()

or
exists(DataFlow::ReadNode base | succ = DataFlow::getUnderlyingNode(base) |
any(DataFlow::Write w).writesElement(base, _, pred) and
[succ.getType(), succ.getType().getPointerType()] instanceof MessageType
Copy link
Collaborator

Choose a reason for hiding this comment

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

Looks like we might need a type that allows us to uniformly treat element/field writes as well as reads?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Combined these by introducing writesComponent

@smowton
Copy link
Contributor Author

smowton commented Sep 3, 2020

@max-schaefer most suggestions implemented except as noted

@smowton
Copy link
Contributor Author

smowton commented Sep 4, 2020

@max-schaefer pushed a fix to the post-update node problems, and an extra probably controversial commit that bypasses PostUpdateNode so we can update objects with longer access paths.

@@ -165,11 +165,14 @@ var CompositeLitExpr = ExprKind.NewBranch("@compositelit")
// ParenExpr is the type of parenthesis expression AST nodes
var ParenExpr = ExprKind.NewBranch("@parenexpr")

// ComponentAccessExpr is the type of field- or element-access nodes
var ComponentAccessExpr = NewUnionType("@componentaccessexpr", NodeType)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, sorry, I didn't mean to suggest that you should do a dbscheme update. That's overkill, and also not the right level of abstraction since a @selectorexpr can be any number of things, not just a field access.

I meant to suggest that you should do this at the QL level:

class ComponentAccess extends Expr {
  ComponentAccess() {
    this instanceof @indexexpr or
     // slightly more complicated reasoning about selectors omitted
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reverted the DB and Expr parts of this

* This is a copy of the logic in `DataFlow::PostUpdateNode`, with the restriction to particular use contexts loosened
* to include any underlying target of a message update.
*/
DataFlow::Node getPreUpdateNode(DataFlow::Node node) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would support extending PostUpdateNodes to do this always. For example, in an assignment

x.y.z = 0

I think it makes sense to consider that both x and x.y have been changed, and hence should have post-update nodes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note that this is a somewhat subtle change and would need careful benchmarking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will certainly have unforeseen consequences. Let's do it, but as its own subsequent PR?

Copy link
Collaborator

Choose a reason for hiding this comment

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

How about trying it out in a separate PR first? This approach is so ugly that I only want to accept it into main once alternatives have been tried and found wanting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dropped this commit for now

Sauyon Lee and others added 7 commits September 4, 2020 15:14
…s taint-flow cases

Only some of the cases are currently working.
This should be either refined to just Message types, or else a macro taint step should be added conducting taint from field-write-of-argument to Marshal's result.

On the read-side we're currently fine: the bytes are tainted, so the object is tainted, so the field reads are tainted.
This is top-level, not a member.
The MarshalState test doesn't work yet, because we don't know to read taint from the Message field of the input or write it to the Buf field of the output
Currently relies on blanket field-write propagation.
@smowton smowton force-pushed the protobufs branch 2 times, most recently from 634dbdf to d4b7129 Compare September 4, 2020 14:29
@smowton
Copy link
Contributor Author

smowton commented Sep 4, 2020

@max-schaefer dropped the post-update node changes to pursue in a future PR, and reverted the DB and Expr.qll changes.

Copy link
Collaborator

@max-schaefer max-schaefer left a comment

Choose a reason for hiding this comment

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

OK, this is looking quite reasonable now. I think it would be worth doing a dist-compare, though, just to make sure.

* Gets the base of `node`, looking through any dereference node found.
*/
DataFlow::Node getBaseLookingThroughDerefs(DataFlow::ComponentReadNode node) {
if node.getBase() instanceof DataFlow::PointerDereferenceNode
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps pull node.getBase() out into a variable. While it may not affect performance much in this case, it's generally best to keep if conditions as simple as possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rewrote to not use if in that case

/**
* Gets the base of `node`, looking through any dereference node found.
*/
DataFlow::Node getBaseLookingThroughDerefs(DataFlow::ComponentReadNode node) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps would could make this predicate private?

marshalStateCall = marshalStateMethod().getACall() and
// pred -> marshalInput.Message
any(DataFlow::Write w)
.writesField(marshalInput.(DataFlow::PostUpdateNode).getPreUpdateNode(),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Perhaps it would be easier to just declare marshalInput as a PostUpdateNode? Then you don't need the cast.

…ritten

For example, writing to a[b].c[d] taints 'a'.
This is the union of a field-access and an element-access instruction
This is its only user for now.
…chain of field- and element-access instructions.

This enables us to use PostUpdateNode properly. Also introduce a test showing a case where this doesn't work, because the underlying variable doesn't have a post-update node.
No behavioural changes
@smowton
Copy link
Contributor Author

smowton commented Sep 4, 2020

@max-schaefer changes applied, running a distcompare now

@smowton
Copy link
Contributor Author

smowton commented Sep 7, 2020

Distcompare results are unremarkable; think this is ready to merge.

@max-schaefer max-schaefer merged commit 1821cca into github:main Sep 7, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants