Skip to content

Commit 12ce3d4

Browse files
committed
Ruby: Implement FileSystemWriteAccess for IO/File API
1 parent 4f0174e commit 12ce3d4

File tree

1 file changed

+150
-70
lines changed

1 file changed

+150
-70
lines changed

ruby/ql/lib/codeql/ruby/frameworks/Files.qll

Lines changed: 150 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,110 @@ private DataFlow::Node fileInstance() {
5252
)
5353
}
5454

55-
private string ioReaderClassMethodName() { result = ["binread", "foreach", "read", "readlines"] }
56-
57-
private string ioReaderInstanceMethodName() {
58-
result =
59-
[
60-
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
61-
"readline", "readlines", "readpartial", "sysread"
62-
]
55+
abstract private class IOOrFileMethodCall extends DataFlow::CallNode {
56+
// TODO: Currently this only handles class method calls.
57+
// Can we infer a path argument for instance method calls?
58+
// e.g. by tracing back to the instantiation of that instance
59+
DataFlow::Node getAPathArgumentImpl() {
60+
result = this.getArgument(0) and this.getReceiverKind() = "class"
61+
}
62+
63+
/**
64+
* Holds if this call appears to read/write from/to a spawned subprocess,
65+
* rather than to/from a file.
66+
*/
67+
predicate spawnsSubprocess() {
68+
pathArgSpawnsSubprocess(this.getAPathArgumentImpl().asExpr().getExpr())
69+
}
70+
71+
/** Gets the API used to perform this call, either "IO" or "File" */
72+
abstract string getAPI();
73+
74+
/** Gets a node representing the data read or written by this call */
75+
abstract DataFlow::Node getADataNodeImpl();
76+
77+
/** Gets a string representation of the receiver kind, either "class" or "instance". */
78+
abstract string getReceiverKind();
6379
}
6480

65-
private string ioReaderMethodName(string receiverKind) {
66-
receiverKind = "class" and result = ioReaderClassMethodName()
67-
or
68-
receiverKind = "instance" and result = ioReaderInstanceMethodName()
81+
/**
82+
* A method call that performs a read using either the `IO` or `File` classes.
83+
*/
84+
private class IOOrFileReadMethodCall extends IOOrFileMethodCall {
85+
private string api;
86+
private string receiverKind;
87+
88+
IOOrFileReadMethodCall() {
89+
exists(string methodName | methodName = this.getMethodName() |
90+
// e.g. `{IO,File}.readlines("foo.txt")`
91+
receiverKind = "class" and
92+
methodName = ["binread", "foreach", "read", "readlines"] and
93+
api = ["IO", "File"] and
94+
this = API::getTopLevelMember(api).getAMethodCall(methodName)
95+
or
96+
// e.g. `{IO,File}.new("foo.txt", "r").getc`
97+
receiverKind = "interface" and
98+
(
99+
methodName =
100+
[
101+
"getbyte", "getc", "gets", "pread", "read", "read_nonblock", "readbyte", "readchar",
102+
"readline", "readlines", "readpartial", "sysread"
103+
] and
104+
(
105+
this.getReceiver() = ioInstance() and api = "IO"
106+
or
107+
this.getReceiver() = fileInstance() and api = "File"
108+
)
109+
)
110+
)
111+
}
112+
113+
override string getAPI() { result = api }
114+
115+
override DataFlow::Node getADataNodeImpl() { result = this }
116+
117+
override string getReceiverKind() { result = receiverKind }
118+
}
119+
120+
/**
121+
* A method call that performs a write using either the `IO` or `File` classes.
122+
*/
123+
private class IOOrFileWriteMethodCall extends IOOrFileMethodCall {
124+
private string api;
125+
private string receiverKind;
126+
private DataFlow::Node dataNode;
127+
128+
IOOrFileWriteMethodCall() {
129+
exists(string methodName | methodName = this.getMethodName() |
130+
// e.g. `{IO,File}.write("foo.txt", "hello\n")`
131+
receiverKind = "class" and
132+
api = ["IO", "File"] and
133+
this = API::getTopLevelMember(api).getAMethodCall(methodName) and
134+
methodName = ["binwrite", "write"] and
135+
dataNode = this.getArgument(1)
136+
or
137+
// e.g. `{IO,File}.new("foo.txt", "a+).puts("hello")`
138+
receiverKind = "interface" and
139+
(
140+
this.getReceiver() = ioInstance() and api = "IO"
141+
or
142+
this.getReceiver() = fileInstance() and api = "File"
143+
) and
144+
(
145+
methodName = ["<<", "print", "putc", "puts", "syswrite", "pwrite", "write_nonblock"] and
146+
dataNode = this.getArgument(0)
147+
or
148+
// Any argument to these methods may be written as data
149+
methodName = ["printf", "write"] and dataNode = this.getArgument(_)
150+
)
151+
)
152+
}
153+
154+
override string getAPI() { result = api }
155+
156+
override DataFlow::Node getADataNodeImpl() { result = dataNode }
157+
158+
override string getReceiverKind() { result = receiverKind }
69159
}
70160

71161
/**
@@ -111,31 +201,31 @@ module IO {
111201
* This class includes only reads that use the `IO` class directly, not those
112202
* that use a subclass of `IO` such as `File`.
113203
*/
114-
class IOReader extends DataFlow::CallNode {
115-
private string receiverKind;
116-
117-
IOReader() {
118-
// `IO` class method calls
119-
receiverKind = "class" and
120-
this = API::getTopLevelMember("IO").getAMethodCall(ioReaderMethodName(receiverKind))
121-
or
122-
// `IO` instance method calls
123-
receiverKind = "instance" and
124-
exists(IOInstanceStrict ii |
125-
this.getReceiver() = ii and
126-
this.getMethodName() = ioReaderMethodName(receiverKind)
127-
)
128-
// TODO: enumeration style methods such as `each`, `foreach`, etc.
129-
}
204+
class IOReader extends IOOrFileReadMethodCall {
205+
IOReader() { this.getAPI() = "IO" }
206+
}
130207

131-
/**
132-
* Gets a string representation of the receiver kind, either "class" or "instance".
133-
*/
134-
string getReceiverKind() { result = receiverKind }
208+
/**
209+
* A `DataFlow::CallNode` that writes data using the `IO` class. For example,
210+
* the `write` and `puts` calls in:
211+
*
212+
* ```rb
213+
* # writes the string `hello world` to the file `foo.txt`
214+
* IO.write("foo.txt", "hello world")
215+
*
216+
* # appends the string `hello again\n` to the file `foo.txt`
217+
* IO.new(IO.sysopen("foo.txt", "a")).puts("hello again")
218+
* ```
219+
*
220+
* This class includes only writes that use the `IO` class directly, not those
221+
* that use a subclass of `IO` such as `File`.
222+
*/
223+
class IOWriter extends IOOrFileWriteMethodCall {
224+
IOWriter() { this.getAPI() = "IO" }
135225
}
136226

137227
/**
138-
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
228+
* A `DataFlow::CallNode` that reads data to the filesystem using the `IO`
139229
* or `File` classes. For example, the `IO.read` and `File#readline` calls in:
140230
*
141231
* ```rb
@@ -146,46 +236,32 @@ module IO {
146236
* File.new("foo.txt").readline
147237
* ```
148238
*/
149-
class FileReader extends DataFlow::CallNode, FileSystemReadAccess::Range {
150-
private string receiverKind;
151-
private string api;
152-
153-
FileReader() {
154-
// A viable `IOReader` that could feasibly read from the filesystem
155-
api = "IO" and
156-
receiverKind = this.(IOReader).getReceiverKind() and
157-
not pathArgSpawnsSubprocess(this.getArgument(0).asExpr().getExpr())
158-
or
159-
api = "File" and
160-
(
161-
// `File` class method calls
162-
receiverKind = "class" and
163-
this = API::getTopLevelMember(api).getAMethodCall(ioReaderMethodName(receiverKind))
164-
or
165-
// `File` instance method calls
166-
receiverKind = "instance" and
167-
exists(File::FileInstance fi |
168-
this.getReceiver() = fi and
169-
this.getMethodName() = ioReaderMethodName(receiverKind)
170-
)
171-
)
172-
// TODO: enumeration style methods such as `each`, `foreach`, etc.
173-
}
239+
class FileReader extends IOOrFileReadMethodCall, FileSystemReadAccess::Range {
240+
FileReader() { not this.spawnsSubprocess() }
174241

175-
// TODO: Currently this only handles class method calls.
176-
// Can we infer a path argument for instance method calls?
177-
// e.g. by tracing back to the instantiation of that instance
178-
override DataFlow::Node getAPathArgument() {
179-
result = this.getArgument(0) and receiverKind = "class"
180-
}
242+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
181243

182-
// This class represents calls that return data
183-
override DataFlow::Node getADataNode() { result = this }
244+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
245+
}
184246

185-
/**
186-
* Returns the most specific core class used for this read, `IO` or `File`
187-
*/
188-
string getAPI() { result = api }
247+
/**
248+
* A `DataFlow::CallNode` that reads data from the filesystem using the `IO`
249+
* or `File` classes. For example, the `write` and `puts` calls in:
250+
*
251+
* ```rb
252+
* # writes the string `hello world` to the file `foo.txt`
253+
* IO.write("foo.txt", "hello world")
254+
*
255+
* # appends the string `hello again\n` to the file `foo.txt`
256+
* File.new("foo.txt", "a").puts("hello again")
257+
* ```
258+
*/
259+
class FileWriter extends IOOrFileWriteMethodCall, FileSystemWriteAccess::Range {
260+
FileWriter() { not this.spawnsSubprocess() }
261+
262+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
263+
264+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
189265
}
190266
}
191267

@@ -231,6 +307,10 @@ module File {
231307
*/
232308
class FileModuleReader extends IO::FileReader {
233309
FileModuleReader() { this.getAPI() = "File" }
310+
311+
override DataFlow::Node getADataNode() { result = this.getADataNodeImpl() }
312+
313+
override DataFlow::Node getAPathArgument() { result = this.getAPathArgumentImpl() }
234314
}
235315

236316
/**

0 commit comments

Comments
 (0)