-
Notifications
You must be signed in to change notification settings - Fork 126
Model websocket read and write functions. #109
Conversation
ghost
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need a little help on handling the io.Reader and io.Writer types. See comments below.
| } | ||
|
|
||
| /** Provides classes for working with Websocket Read calls. */ | ||
| module WebsocketReadFunction { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This module would be merged with the websocket.qll from the other PR. Since that has not been merged yet, I created a new file for this PR.
| private class GolangXNetCodecRecvMethod extends Range { | ||
| GolangXNetCodecRecvMethod() { | ||
| // func (cd Codec) Receive(ws *Conn, v interface{}) (err error) | ||
| this.getTarget().(Method).hasQualifiedName("golang.org/x/net/websocket", "Codec", "Receive") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hardcoded golang.org/x/net/websocket would be replaced before merge.
|
#129 might help with this, though more API modeling is probably needed. |
|
I have re-modeled the api. The getSink calls are now gone. I just return the source/sink directly. I have also added library tests for the functions. As of now, all but one test cases are detected. The query fails to detect the following block of code. codeql-go/ql/test/library-tests/semmle/go/frameworks/Websocket/WebsocketReadWrite.go Lines 40 to 42 in 0023c06
This is because I haven't modelled the io package here. I am sending in a separate PR for the same soon. I think this is ready for a review otherwise. |
|
The test fail is expected as I haven't included the |
sauyon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've stubbed dependencies for you and removed the changes to the top-level go.mod.
| "github.com/gobwas/ws" | ||
| gobwas "github.com/gobwas/ws" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is gobwas imported twice here?
sauyon
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some initial comments.
| where cfg.hasFlowPath(source, sink) | ||
| select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.", | ||
| source.getNode(), "user-provided value" | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By definition, a file is a valid file only if it has a newline at the end. Most applications don't depend on this but the auto-formatter added so I kept it as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but you've added trailing whitespace to that line, which is what I wanted to get rid of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried removing that and running auto format running it adds it again. I can see there's only one new line at the end of the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nvm, I figured it out. It was a issue on my side. I have fixed it now
| * Extends this class to refine existing API models. If you want to model new APIs, | ||
| * extend `WebsocketReadFunction::Range` instead. | ||
| */ | ||
| class WebsocketRead extends DataFlow::Node, UntrustedFlowSource::Range { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this should be called WebsocketMessage instead?
|
|
||
| /** | ||
| * A message received from a websocket connection using `Receive` method of | ||
| * the https://golang.org/x/net/websocket package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * the https://golang.org/x/net/websocket package. | |
| * the [golang.org/x/net/websocket](https://golang.org/x/net/websocket) package. |
And similar below.
| /** | ||
| * A data-flow node that represents data received from a websocket connection | ||
| * | ||
| * Extends this class to model new APIs. If you want to refine existing API models, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Extends this class to model new APIs. If you want to refine existing API models, | |
| * Extend this class to model new APIs. If you want to refine existing API models, |
| /** | ||
| * A message received from a websocket connection. | ||
| * | ||
| * Extends this class to refine existing API models. If you want to model new APIs, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * Extends this class to refine existing API models. If you want to model new APIs, | |
| * Extend this class to refine existing API models. If you want to model new APIs, |
(It seems this is an error copy-pasted from a few other places in the codebase; maybe you could sed -i 's/Extends this class/Extend this class' or something like that?)
| * A message received from a websocket connection using `Read` method of | ||
| * the https://golang.org/x/net/websocket package. | ||
| */ | ||
| private class GolangXNetConnReadMethod extends Range { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these names are a bit misleading, since they're not actually methods or functions.
Maybe just call them Messages?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I note, though, that in each case this is bound to an exit node of a FunctionOutput, so it seems to me like these classes really do want to be functions, not data-flow nodes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think, we can rename the classes from something like GolangXNetConnReadMethod to GolangXNetConnRead. It is,in principle true, they are not functions or methods but calling them *Message is also not ideal. They may represent writers or other types too. WDYT?
| exists(DataFlow::CallNode m, string tp | | ||
| m.getTarget().hasQualifiedName("github.com/gobwas/ws", tp) and | ||
| (tp = "ReadFrame" or tp = "ReadHeader") and | ||
| this.(DataFlow::SsaNode).getInit() = m.getResult(0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| this.(DataFlow::SsaNode).getInit() = m.getResult(0) | |
| this = m.getResult(0) |
I think this should suffice for simple values like these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And also abov: the SsaNode.getInit trick should only be needed for results that are writers, I think.
| * Extends this class to refine existing API models. If you want to model new APIs, | ||
| * extend `WebsocketReadFunction::Range` instead. | ||
| */ | ||
| class WebsocketWrite extends DataFlow::Node { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As above, I wonder whether it might be easier to model functions instead of data-flow nodes here, and specify for each function which FunctionInput is written to a web socket.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure I understand. Do you mean having something like this?
class A extends Function {
getSink(){result= argument}
}or
class A extends FunctionNode{
getSink(){
result = parameter
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like
class A extends Function {
FunctionOutput getContent() {
result.isParameter(1) // or similar
}
}I would think.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What @sauyon says, though I think it should be FunctionInput.
Concretely, I would suggest rewriting WebsocketWrite as
class WebSocketWriter extends Function {
WebSocketWriter::Range self;
WebSocketWriter() { this = self }
/** Gets an input to this function that is written to a WebSocket connection. */
FunctionInput getAnInput() { result = self.getAnInput() }
}and then define
module WebSocketWriter {
abstract class Range extends Function {
abstract FunctionInput getAnInput();
}
private class GolangXNetCodecSend extends Range, Method {
GolangXNetCodecSend() { this.hasQualifiedName("golang.org/x/net/websocket", "Codec", "Send") }
FunctionInput getAnInput() { result.isParameter(1) }
}
...
}Data-flow nodes that can be written to a WebSocket can then be identified as
exists(WebSocketWriter w |
nd = w.getAnInput().getNode(w.getACall())
)As you can see, this simplifies the definitions of the subclasses of WebSocketWriter::Range by separating the problem of identifying which function inputs get written to a WebSocket from finding the corresponding nodes for a concrete invocation.
The same comments apply to the modelling of WebSocket reads above, but with FunctionOutput instead of FunctionInput.
|
Hey! can someone check what's wrong with the CI? I run tests on my local node, they pass but on the CI they fail. |
I take it you have resolved this issue? The tests now seem to pass. |
max-schaefer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments and clarifications. Thanks for persevering through what I realise is quite a drawn-out review process. We really appreciate your contributions!
@sauyon, it looks like you haven't added the LICENSE files for the stubbed libraries yet.
|
|
||
| /** | ||
| * A message received from a websocket connection using `Receive` method of | ||
| * the [https://golang.org/x/net/websocket](https://golang.org/x/net/websocket) package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| * the [https://golang.org/x/net/websocket](https://golang.org/x/net/websocket) package. | |
| * the https://golang.org/x/net/websocket package. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are a few other instances of spurious multi-whitespace after * below. I trust you can find them even though I have not flagged them up individually.
| * Extends this class to refine existing API models. If you want to model new APIs, | ||
| * extend `WebsocketReadFunction::Range` instead. | ||
| */ | ||
| class WebsocketWrite extends DataFlow::Node { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What @sauyon says, though I think it should be FunctionInput.
Concretely, I would suggest rewriting WebsocketWrite as
class WebSocketWriter extends Function {
WebSocketWriter::Range self;
WebSocketWriter() { this = self }
/** Gets an input to this function that is written to a WebSocket connection. */
FunctionInput getAnInput() { result = self.getAnInput() }
}and then define
module WebSocketWriter {
abstract class Range extends Function {
abstract FunctionInput getAnInput();
}
private class GolangXNetCodecSend extends Range, Method {
GolangXNetCodecSend() { this.hasQualifiedName("golang.org/x/net/websocket", "Codec", "Send") }
FunctionInput getAnInput() { result.isParameter(1) }
}
...
}Data-flow nodes that can be written to a WebSocket can then be identified as
exists(WebSocketWriter w |
nd = w.getAnInput().getNode(w.getACall())
)As you can see, this simplifies the definitions of the subclasses of WebSocketWriter::Range by separating the problem of identifying which function inputs get written to a WebSocket from finding the corresponding nodes for a concrete invocation.
The same comments apply to the modelling of WebSocket reads above, but with FunctionOutput instead of FunctionInput.
|
As I had mentioned above, #109 (comment) There was a test case involving I have updated the query and added the new result. I have also squashed and merged all the commits into a single one for easier merging. Please note, while this can now merged, please don't run an evaluation against all lgtm projects just yet. I will model the |
max-schaefer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly LGTM, modulo a few typos and missing licenses for stubbed test dependencies (@sauyon). I have started an evaluation.
| // Code generated by depstubber. DO NOT EDIT. | ||
| // This is a simple stub for nhooyr.io/websocket, strictly for use in testing. | ||
|
|
||
| // See the LICENSE file for information about the licensing of the original library. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sauyon: I think you forgot to include the LICENSE file when stubbing. (Time to do something about that warning you mentioned?)
| */ | ||
| private class GolangXNetCodecSend extends Range, Method { | ||
| GolangXNetCodecSend() { | ||
| // func (cd Codec) Receive(ws *Conn, v interface{}) (err error) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should presumably be Send, but I'm not sure about the function signature.
|
Added licenses, sorry. |
|
@max-schaefer The message is directly returned to the connection without sanitization. This allows an attacker to potentially send in JS code. When this is received again on the client side, the code will be execute resulting in an alert. $("html").append("<html>"+ws.receive()+"</html>")Since the message is reflected back, You can see even in the corresponding non websocket XSS testcase, see that we alert it as an xss when the username parameter is written directly to the http response. |
|
We generally try to exclude things that require client-side code in order to be exploitable in order to avoid false positives. In this case in particular, I would guess most websocket responses are not written to an HTML body, and are therefore not exploitable. Is there some reason that isn't the case?
For that example, a user clicking a malicious link to that page would immediately be vulnerable, with no client-side code needed. |
|
@porcupineyhairs, thank you for your explanation. I find it quite implausible that this is what's happening in this case, but maybe you have some evidence that it is? I realise that there are scenarios where an unsuspecting user might be tricked into doing something that sends unsanitised data across a WebSocket, and the reflected data is then embedded into HTML. But detecting that based purely on server-side code (as we are doing here) seems difficult if not impossible, and you don't seem to have implemented any logic that even tries to do that. As it stands, the potential for false positives seems way too high to me. |
|
Please allow me to give you a more elaborate explaination. First off, the query adds support for read as well as write functions. Since an attacker may control data which is read by a server, all reads on the server side should be marked as remote flow sources. (I can see I have made an error and added WebSocket read as a source for only the XSS query. This was included initially but got lost somewhere durign the review. I have corrected it now and included with the other remote flow sources. You should try running an evaluation again now.)
While technically you can use WebSockets to send other forms of data such as images or templates, it seems unlikely any application does it in real life. Traditional HTTP stack with caching usually in several layers beats WebSockets any time for serving images and other binary data. Plus browsers started supporting WebSockets only up until recently so you loose backwards compatibility for practially nothing. The case which Max pointed out is not an actual application but rather a test for RFC standard compliance. As you may have noticed, some of the API's make do a distinction between binary and text messages. Most of the users of these API's from what I could see are chat applications and the like who use the binary mode to send encoded JSON/protobuf streams. The encoding is then decoded on the client side and the original message is obtained. The message in most cases at least in part, ends up in the html. Hence I made the decision to include writes to the websocket connection as well. However, I may be wrong here as I can't back my claims by any real stats. I haven't seen the results of the eval yet. If there are not that many FP's, I would recommend that we keep the writes as is. If there are too many FP's, here's what I propose.
Please let me know what your thoughts are here. |
A sweeping claim like that isn't very convincing without supporting data, I'm afraid. I think @sauyon put it best: you can't tell from a WebSocket write how the data written to it will be used, so in the interest of avoiding false positives we will need to assume that it is safe. I am, however, fine with treating data read from a WebSocket as untrusted, so I would suggest simply removing the extra XSS sink. What do you think, @sauyon? |
|
That sounds sensible to me. |
|
I have rebased and squashed the changes. You can now start an evaluation. |
|
Since this isn't the first unrelated/nonsensical comment we're seeing from this user, I have reported them for abuse. (EDIT: I was referring to the comment Sauyon deleted.) |
|
With #107 having gone in, this now needs conflict resolution. |
max-schaefer
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Evaluation looks fine, so this is good to go in. Many thanks for your contribution!
|
No change note required as it's covered by change-notes/2020-05-22-websocket-model.md |
No description provided.