Merge pull request #992 from xiemaisi/js/socket.io

Approved by asger-semmle
This commit is contained in:
semmle-qlci
2019-02-27 18:43:40 +00:00
committed by GitHub
27 changed files with 464 additions and 0 deletions

View File

@@ -79,6 +79,7 @@ import semmle.javascript.frameworks.React
import semmle.javascript.frameworks.ReactNative
import semmle.javascript.frameworks.Request
import semmle.javascript.frameworks.SQL
import semmle.javascript.frameworks.SocketIO
import semmle.javascript.frameworks.StringFormatters
import semmle.javascript.frameworks.UriLibraries
import semmle.javascript.frameworks.Vue

View File

@@ -0,0 +1,239 @@
/**
* Provides classes for working with [socket.io](https://socket.io).
*/
import javascript
private import semmle.javascript.dataflow.InferredTypes
/**
* Provides classes for working with server-side socket.io code.
*
* We model three concepts: servers, namespaces, and sockets. A server
* has one or more namespaces associated with it, each identified by
* a path name. There is always a default namespace associated with the
* path "/". Data flows between client and server side through sockets,
* with each socket belonging to a namespace on a server.
*/
module SocketIO {
/** Gets the name of a method on `EventEmitter` that returns `this`. */
private string chainableEventEmitterMethod() {
result = "off" or
result = "on" or
result = "once" or
result = "prependListener" or
result = "prependOnceListener" or
result = "removeAllListeners" or
result = "removeListener" or
result = "setMaxListeners"
}
/** A data flow node that may produce (that is, create or return) a socket.io server. */
class ServerNode extends DataFlow::SourceNode {
ServerNode() {
// server creation
this = DataFlow::moduleImport("socket.io").getAnInvocation()
or
// alias for `Server`
this = DataFlow::moduleImport("socket.io").getAMemberCall("listen")
or
// invocation of a chainable method
exists(DataFlow::MethodCallNode mcn, string m |
m = "adapter" or
m = "attach" or
m = "bind" or
m = "listen" or
m = "onconnection" or
m = "origins" or
m = "path" or
m = "serveClient" or
m = "set"
|
mcn = any(ServerNode srv).getAMethodCall(m) and
// exclude getter versions
not mcn.getNumArgument() = 0 and
this = mcn
)
}
}
/** A data flow node that may produce a namespace object. */
class NamespaceNode extends DataFlow::SourceNode {
NamespaceNode() {
// namespace lookup
exists(ServerNode srv |
this = srv.getAPropertyRead("sockets")
or
this = srv.getAMethodCall("of")
)
or
// invocation of a chainable method
exists(string m |
m = "binary" or
m = "clients" or
m = "compress" or
m = "emit" or
m = "in" or
m = "send" or
m = "to" or
m = "use" or
m = "write" or
m = chainableEventEmitterMethod()
|
this = any(NamespaceNode ns).getAMethodCall(m)
or
// server objects forward these methods to their default namespace
this = any(ServerNode srv).getAMethodCall(m)
)
or
// invocation of chainable getter method
exists(string m |
m = "json" or
m = "local" or
m = "volatile"
|
this = any(NamespaceNode base).getAPropertyRead(m)
)
}
}
/** A data flow node that may produce a socket object. */
class SocketNode extends DataFlow::SourceNode {
SocketNode() {
// callback accepting a socket
exists(DataFlow::SourceNode base, string connect, DataFlow::MethodCallNode on |
(base instanceof ServerNode or base instanceof NamespaceNode) and
(connect = "connect" or connect = "connection")
|
on = base.getAMethodCall("on") and
on.getArgument(0).mayHaveStringValue(connect) and
this = on.getCallback(1).getParameter(0)
)
or
// invocation of a chainable method
exists(string m |
m = "binary" or
m = "compress" or
m = "disconnect" or
m = "emit" or
m = "in" or
m = "join" or
m = "leave" or
m = "send" or
m = "to" or
m = "use" or
m = "write" or
m = chainableEventEmitterMethod()
|
this = any(SocketNode base).getAMethodCall(m)
)
or
// invocation of a chainable getter method
exists(string m |
m = "broadcast" or
m = "json" or
m = "local" or
m = "volatile"
|
this = any(SocketNode base).getAPropertyRead(m)
)
}
}
/**
* A data flow node representing an API call that receives data from a client.
*/
class ReceiveNode extends DataFlow::MethodCallNode {
SocketNode socket;
DataFlow::Node eventName;
ReceiveNode() {
exists(string on |
on = "addListener" or
on = "on" or
on = "once" or
on = "prependListener" or
on = "prependOnceListener"
|
this = socket.getAMethodCall(on) and
eventName = getArgument(0)
)
}
/** Gets the socket through which data is received. */
SocketNode getSocket() { result = socket }
/** Gets the event name associated with the data, if it can be determined. */
string getEventName() { eventName.mayHaveStringValue(result) }
/** Gets a data flow node representing data received from a client. */
DataFlow::SourceNode getAReceivedItem() { result = getCallback(1).getAParameter() }
}
/**
* A data flow node representing data received from a client, viewed as remote user input.
*/
private class ReceivedItemAsRemoteFlow extends RemoteFlowSource {
ReceivedItemAsRemoteFlow() { this = any(ReceiveNode rercv).getAReceivedItem() }
override string getSourceType() { result = "socket.io client data" }
override predicate isUserControlledObject() { any() }
}
/**
* A data flow node representing an API call that sends data to a client.
*/
class SendNode extends DataFlow::MethodCallNode {
DataFlow::SourceNode base;
int firstDataIndex;
SendNode() {
exists(string m |
(base instanceof ServerNode or base instanceof NamespaceNode or base instanceof SocketNode) and
this = base.getAMethodCall(m)
|
// a call to `emit`
m = "emit" and
firstDataIndex = 1
or
// a call to `send` or `write`
(m = "send" or m = "write") and
firstDataIndex = 0
)
}
/**
* Gets the socket through which data is sent to the client.
*
* This predicate is not defined for broadcasting sends.
*/
SocketNode getSocket() { result = base }
/** Gets the event name associated with the data, if it can be determined. */
string getEventName() {
if firstDataIndex = 1 then getArgument(0).mayHaveStringValue(result) else result = "message"
}
/** Gets a data flow node representing data sent to the client. */
DataFlow::Node getASentItem() {
exists(int i |
result = getArgument(i) and
i >= firstDataIndex and
// exclude last argument if it is a callback
(
i < getNumArgument() - 1 or
not result.analyze().getTheType() = TTFunction()
)
)
}
/** Gets the acknowledgment callback, if any. */
DataFlow::FunctionNode getAck() {
// acknowledgments are only available when sending through a socket
exists(getSocket()) and
result = getLastArgument().getALocalSource()
}
}
}

View File

@@ -0,0 +1,27 @@
| tst.js:25:10:25:19 | io.sockets |
| tst.js:26:1:26:10 | io.of("/") |
| tst.js:27:1:27:12 | ns.use(auth) |
| tst.js:28:1:28:11 | ns.to(room) |
| tst.js:29:1:29:11 | ns.in(room) |
| tst.js:30:1:30:28 | ns.emit ... event') |
| tst.js:31:1:31:20 | ns.send('a message') |
| tst.js:32:1:32:21 | ns.writ ... ssage') |
| tst.js:33:1:33:14 | ns.clients(cb) |
| tst.js:34:1:34:17 | ns.compress(true) |
| tst.js:35:1:35:16 | ns.binary(false) |
| tst.js:36:1:36:12 | io.use(auth) |
| tst.js:37:1:37:11 | io.to(room) |
| tst.js:38:1:38:11 | io.in(room) |
| tst.js:39:1:39:31 | io.emit ... ssage') |
| tst.js:40:1:40:20 | io.send('a message') |
| tst.js:41:1:41:21 | io.writ ... ssage') |
| tst.js:42:1:42:14 | io.clients(cb) |
| tst.js:43:1:43:17 | io.compress(true) |
| tst.js:44:1:44:16 | io.binary(false) |
| tst.js:45:1:45:7 | ns.json |
| tst.js:46:1:46:11 | ns.volatile |
| tst.js:47:1:47:8 | ns.local |
| tst.js:50:1:66:2 | io.on(' ... cal;\\n}) |
| tst.js:67:1:67:35 | io.on(' ... => {}) |
| tst.js:68:1:68:32 | ns.on(' ... => {}) |
| tst.js:69:1:73:2 | ns.on(' ... {});\\n}) |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::NamespaceNode ns
select ns

View File

@@ -0,0 +1,3 @@
| tst.js:70:3:70:35 | socket. ... => {}) | tst.js:69:22:69:27 | socket |
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:69:22:69:27 | socket |
| tst.js:72:3:72:43 | socket. ... => {}) | tst.js:69:22:69:27 | socket |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::ReceiveNode rn
select rn, rn.getSocket()

View File

@@ -0,0 +1,3 @@
| tst.js:70:3:70:35 | socket. ... => {}) | tst.js:70:25:70:27 | msg |
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:71:27:71:31 | data1 |
| tst.js:71:3:71:46 | socket. ... => {}) | tst.js:71:34:71:38 | data2 |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::ReceiveNode rn
select rn, rn.getAReceivedItem()

View File

@@ -0,0 +1,2 @@
| tst.js:70:3:70:35 | socket. ... => {}) | message |
| tst.js:71:3:71:46 | socket. ... => {}) | message |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::ReceiveNode rn
select rn, rn.getEventName()

View File

@@ -0,0 +1,9 @@
| tst.js:30:1:30:28 | ns.emit ... event') |
| tst.js:31:1:31:20 | ns.send('a message') |
| tst.js:32:1:32:21 | ns.writ ... ssage') |
| tst.js:39:1:39:31 | io.emit ... ssage') |
| tst.js:40:1:40:20 | io.send('a message') |
| tst.js:41:1:41:21 | io.writ ... ssage') |
| tst.js:51:3:51:22 | socket.emit('event') |
| tst.js:54:3:54:43 | socket. ... => {}) |
| tst.js:55:3:55:27 | socket. ... ssage') |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::SendNode sn
select sn

View File

@@ -0,0 +1,9 @@
| tst.js:30:1:30:28 | ns.emit ... event') | tst.js:30:18:30:27 | 'an event' |
| tst.js:31:1:31:20 | ns.send('a message') | tst.js:31:9:31:19 | 'a message' |
| tst.js:32:1:32:21 | ns.writ ... ssage') | tst.js:32:10:32:20 | 'a message' |
| tst.js:39:1:39:31 | io.emit ... ssage') | tst.js:39:20:39:30 | 'a message' |
| tst.js:40:1:40:20 | io.send('a message') | tst.js:40:9:40:19 | 'a message' |
| tst.js:41:1:41:21 | io.writ ... ssage') | tst.js:41:10:41:20 | 'a message' |
| tst.js:54:3:54:43 | socket. ... => {}) | tst.js:54:15:54:17 | 'a' |
| tst.js:54:3:54:43 | socket. ... => {}) | tst.js:54:20:54:28 | 'message' |
| tst.js:55:3:55:27 | socket. ... ssage') | tst.js:55:16:55:26 | 'a message' |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::SendNode sn
select sn, sn.getASentItem()

View File

@@ -0,0 +1 @@
| tst.js:54:3:54:43 | socket. ... => {}) | tst.js:54:31:54:42 | (data) => {} |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::SendNode sn
select sn, sn.getAck()

View File

@@ -0,0 +1,3 @@
| tst.js:51:3:51:22 | socket.emit('event') | tst.js:50:19:50:24 | socket |
| tst.js:54:3:54:43 | socket. ... => {}) | tst.js:50:19:50:24 | socket |
| tst.js:55:3:55:27 | socket. ... ssage') | tst.js:50:19:50:24 | socket |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::SendNode sn
select sn, sn.getSocket()

View File

@@ -0,0 +1,12 @@
| tst.js:1:12:1:33 | require ... .io')() |
| tst.js:4:13:4:24 | new Server() |
| tst.js:6:13:6:27 | Server.listen() |
| tst.js:9:1:9:21 | io.serv ... (false) |
| tst.js:10:1:10:21 | io.set( ... s', []) |
| tst.js:11:1:11:21 | io.path ... npath') |
| tst.js:12:1:12:15 | io.adapter(foo) |
| tst.js:13:1:13:14 | io.origins([]) |
| tst.js:14:1:14:15 | io.listen(http) |
| tst.js:15:1:15:15 | io.attach(http) |
| tst.js:16:1:16:15 | io.bind(engine) |
| tst.js:17:1:17:23 | io.onco ... socket) |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::ServerNode srv
select srv

View File

@@ -0,0 +1,21 @@
| tst.js:50:19:50:24 | socket |
| tst.js:51:3:51:22 | socket.emit('event') |
| tst.js:52:3:52:17 | socket.to(room) |
| tst.js:53:3:53:17 | socket.in(room) |
| tst.js:54:3:54:43 | socket. ... => {}) |
| tst.js:55:3:55:27 | socket. ... ssage') |
| tst.js:56:3:56:19 | socket.join(room) |
| tst.js:57:3:57:20 | socket.leave(room) |
| tst.js:58:3:58:16 | socket.use(cb) |
| tst.js:59:3:59:23 | socket. ... s(true) |
| tst.js:60:3:60:22 | socket.binary(false) |
| tst.js:61:3:61:25 | socket. ... t(true) |
| tst.js:62:3:62:13 | socket.json |
| tst.js:63:3:63:17 | socket.volatile |
| tst.js:64:3:64:18 | socket.broadcast |
| tst.js:65:3:65:14 | socket.local |
| tst.js:67:22:67:27 | socket |
| tst.js:68:19:68:24 | socket |
| tst.js:69:22:69:27 | socket |
| tst.js:70:3:70:35 | socket. ... => {}) |
| tst.js:71:3:71:46 | socket. ... => {}) |

View File

@@ -0,0 +1,4 @@
import javascript
from SocketIO::SocketNode sn
select sn

View File

@@ -0,0 +1,73 @@
const io = require('socket.io')(); // SocketIO::ServerNode
const Server = require('socket.io');
const io2 = new Server(); // SocketIO::ServerNode
const io3 = Server.listen(); // SocketIO::ServerNode
// more SocketIO::ServerNodes:
io.serveClient(false);
io.set('origins', []);
io.path('/myownpath');
io.adapter(foo);
io.origins([]);
io.listen(http);
io.attach(http);
io.bind(engine);
io.onconnection(socket);
// not SocketIO::ServerNodes:
io.path();
io.adapter();
io.origins();
// SocketIO::NamespaceNodes:
var ns = io.sockets;
io.of("/");
ns.use(auth);
ns.to(room);
ns.in(room);
ns.emit('event', 'an event');
ns.send('a message');
ns.write('a message');
ns.clients(cb);
ns.compress(true);
ns.binary(false);
io.use(auth);
io.to(room);
io.in(room);
io.emit('message', 'a message');
io.send('a message');
io.write('a message');
io.clients(cb);
io.compress(true);
io.binary(false);
ns.json;
ns.volatile;
ns.local;
// SocketIO::SocketNodes:
io.on('connect', (socket) => {
socket.emit('event');
socket.to(room);
socket.in(room);
socket.send('a', 'message', (data) => {});
socket.write('a message');
socket.join(room);
socket.leave(room);
socket.use(cb);
socket.compress(true);
socket.binary(false);
socket.disconnect(true);
socket.json;
socket.volatile;
socket.broadcast;
socket.local;
});
io.on('connection', (socket) => {});
ns.on('connect', (socket) => {});
ns.on('connection', (socket) => {
socket.on('message', (msg) => {});
socket.once('message', (data1, data2) => {});
socket.addListener(eventName(), () => {});
});

View File

@@ -45,6 +45,9 @@ nodes
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title |
| mongooseJsonParse.js:20:30:20:43 | req.query.data |
| mongooseJsonParse.js:23:19:23:23 | query |
| socketio.js:10:25:10:30 | handle |
| socketio.js:11:12:11:53 | `INSERT ... andle}` |
| socketio.js:11:46:11:51 | handle |
| tst2.js:9:27:9:78 | "select ... rams.id |
| tst2.js:9:27:9:84 | "select ... d + "'" |
| tst2.js:9:66:9:78 | req.params.id |
@@ -125,6 +128,8 @@ edges
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:19:19:19:20 | {} |
| mongooseJsonParse.js:20:19:20:50 | JSON.pa ... ).title | mongooseJsonParse.js:23:19:23:23 | query |
| mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:20:19:20:44 | JSON.pa ... y.data) |
| socketio.js:10:25:10:30 | handle | socketio.js:11:46:11:51 | handle |
| socketio.js:11:46:11:51 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` |
| tst2.js:9:27:9:78 | "select ... rams.id | tst2.js:9:27:9:84 | "select ... d + "'" |
| tst2.js:9:66:9:78 | req.params.id | tst2.js:9:27:9:78 | "select ... rams.id |
| tst3.js:8:7:9:55 | query1 | tst3.js:10:14:10:19 | query1 |
@@ -154,6 +159,7 @@ edges
| mongoose.js:60:25:60:29 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:60:25:60:29 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |
| mongoose.js:63:24:63:28 | query | mongoose.js:21:19:21:26 | req.body | mongoose.js:63:24:63:28 | query | This query depends on $@. | mongoose.js:21:19:21:26 | req.body | a user-provided value |
| mongooseJsonParse.js:23:19:23:23 | query | mongooseJsonParse.js:20:30:20:43 | req.query.data | mongooseJsonParse.js:23:19:23:23 | query | This query depends on $@. | mongooseJsonParse.js:20:30:20:43 | req.query.data | a user-provided value |
| socketio.js:11:12:11:53 | `INSERT ... andle}` | socketio.js:10:25:10:30 | handle | socketio.js:11:12:11:53 | `INSERT ... andle}` | This query depends on $@. | socketio.js:10:25:10:30 | handle | a user-provided value |
| tst2.js:9:27:9:84 | "select ... d + "'" | tst2.js:9:66:9:78 | req.params.id | tst2.js:9:27:9:84 | "select ... d + "'" | This query depends on $@. | tst2.js:9:66:9:78 | req.params.id | a user-provided value |
| tst3.js:10:14:10:19 | query1 | tst3.js:9:16:9:34 | req.params.category | tst3.js:10:14:10:19 | query1 | This query depends on $@. | tst3.js:9:16:9:34 | req.params.category | a user-provided value |
| tst4.js:8:10:8:66 | 'SELECT ... d + '"' | tst4.js:8:46:8:60 | $routeParams.id | tst4.js:8:10:8:66 | 'SELECT ... d + '"' | This query depends on $@. | tst4.js:8:46:8:60 | $routeParams.id | a user-provided value |

View File

@@ -0,0 +1,13 @@
// Adapted from https://github.com/mapbox/node-sqlite3/wiki/API, which is
// part of the node-sqlite3 project, which is licensed under the BSD 3-Clause
// License; see file node-sqlite3-LICENSE.
var express = require('express');
var sqlite3 = require('sqlite3').verbose();
var db = new sqlite3.Database(':memory:');
var io = require('socket.io')();
io.on('connection', (socket) => {
socket.on('newuser', (handle) => {
db.run(`INSERT INTO users(name) VALUES ${handle}`);
});
});