Skip to content

Commit d1f2258

Browse files
committed
revamp weak file permissions query
1 parent 25300cb commit d1f2258

File tree

5 files changed

+204
-112
lines changed

5 files changed

+204
-112
lines changed

ql/lib/codeql/ruby/Concepts.qll

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,45 +37,107 @@ module SqlExecution {
3737
}
3838

3939
/**
40-
* A data flow node that performs a file system access (read, write, copy, permissions, stats, etc).
40+
* A data flow node that performs a file system access, including reading and writing data,
41+
* creating and deleting files and folders, checking and updating permissions, and so on.
42+
*
43+
* Extend this class to refine existing API models. If you want to model new APIs,
44+
* extend `FileSystemAccess::Range` instead.
4145
*/
42-
abstract class FileSystemAccess extends DataFlow::Node {
46+
class FileSystemAccess extends DataFlow::Node {
47+
FileSystemAccess::Range range;
48+
49+
FileSystemAccess() { this = range }
50+
4351
/** Gets an argument to this file system access that is interpreted as a path. */
44-
abstract DataFlow::Node getAPathArgument();
52+
DataFlow::Node getAPathArgument() { result = range.getAPathArgument() }
53+
}
4554

55+
/** Provides a class for modeling new file system access APIs. */
56+
module FileSystemAccess {
4657
/**
47-
* Gets an argument to this file system access that is interpreted as a root folder
48-
* in which the path arguments are constrained.
58+
* A data-flow node that performs a file system access, including reading and writing data,
59+
* creating and deleting files and folders, checking and updating permissions, and so on.
4960
*
50-
* In other words, if a root argument is provided, the underlying file access does its own
51-
* sanitization to prevent the path arguments from traversing outside the root folder.
61+
* Extend this class to model new APIs. If you want to refine existing API models,
62+
* extend `FileSystemAccess` instead.
63+
*/
64+
abstract class Range extends DataFlow::Node {
65+
/** Gets an argument to this file system access that is interpreted as a path. */
66+
abstract DataFlow::Node getAPathArgument();
67+
}
68+
}
69+
70+
/**
71+
* A data flow node that reads data from the file system.
72+
*
73+
* Extend this class to refine existing API models. If you want to model new APIs,
74+
* extend `FileSystemReadAccess::Range` instead.
75+
*/
76+
class FileSystemReadAccess extends FileSystemAccess {
77+
override FileSystemReadAccess::Range range;
78+
79+
/**
80+
* Gets a node that represents data read from the file system access.
5281
*/
53-
DataFlow::Node getRootPathArgument() { none() }
82+
DataFlow::Node getADataNode() { result = range.getADataNode() }
83+
}
5484

85+
/** Provides a class for modeling new file system reads. */
86+
module FileSystemReadAccess {
5587
/**
56-
* Holds if this file system access will reject paths containing upward navigation
57-
* segments (`../`).
88+
* A data flow node that reads data from the file system.
5889
*
59-
* `argument` should refer to the relevant path argument or root path argument.
90+
* Extend this class to model new APIs. If you want to refine existing API models,
91+
* extend `FileSystemReadAccess` instead.
6092
*/
61-
predicate isUpwardNavigationRejected(DataFlow::Node argument) { none() }
93+
abstract class Range extends FileSystemAccess::Range {
94+
/**
95+
* Gets a node that represents data read from the file system.
96+
*/
97+
abstract DataFlow::Node getADataNode();
98+
}
6299
}
63100

64101
/**
65-
* A data flow node that reads data from the file system.
102+
* A data flow node that sets the permissions for one or more files.
103+
*
104+
* Extend this class to refine existing API models. If you want to model new APIs,
105+
* extend `FileSystemPermissionModification::Range` instead.
66106
*/
67-
abstract class FileSystemReadAccess extends FileSystemAccess {
68-
/** Gets a node that represents data from the file system. */
69-
abstract DataFlow::Node getADataNode();
107+
class FileSystemPermissionModification extends DataFlow::Node {
108+
FileSystemPermissionModification::Range range;
109+
110+
FileSystemPermissionModification() { this = range }
111+
112+
/**
113+
* Gets an argument to this permission modification that is interpreted as a
114+
* set of permissions.
115+
*/
116+
DataFlow::Node getAPermissionNode() { result = range.getAPermissionNode() }
70117
}
71118

119+
/** Provides a class for modeling new file system permission modifications. */
120+
module FileSystemPermissionModification {
121+
/**
122+
* A data-flow node that sets permissions for a one or more files.
123+
*
124+
* Extend this class to model new APIs. If you want to refine existing API models,
125+
* extend `FileSystemPermissionModification` instead.
126+
*/
127+
abstract class Range extends DataFlow::Node {
128+
/**
129+
* Gets an argument to this permission modification that is interpreted as a
130+
* set of permissions.
131+
*/
132+
abstract DataFlow::Node getAPermissionNode();
133+
}
134+
}
72135

73136
/**
74137
* A data flow node that contains a file name or an array of file names from the local file system.
75138
*/
76139
abstract class FileNameSource extends DataFlow::Node { }
77140

78-
79141
/**
80142
* A data-flow node that escapes meta-characters, which could be used to prevent
81143
* injection attacks.

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

Lines changed: 69 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,49 +6,84 @@ private import ruby
66
private import codeql.ruby.Concepts
77
private import codeql.ruby.ApiGraphs
88

9-
/** A permissions argument of a call to a File/FileUtils method that may modify file permissions */
10-
/*
11-
class PermissionArgument extends DataFlow::Node {
12-
private DataFlow::CallNode call;
13-
14-
PermissionArgument() {
15-
exists(string methodName |
16-
call = API::getTopLevelMember(["File", "FileUtils"]).getAMethodCall(methodName)
17-
|
18-
methodName in ["chmod", "chmod_R", "lchmod"] and this = call.getArgument(0)
19-
or
20-
methodName = "mkfifo" and this = call.getArgument(1)
21-
or
22-
methodName in ["new", "open"] and this = call.getArgument(2)
23-
or
24-
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
25-
this = call.getKeywordArgument("mode")
26-
// TODO: defaults for optional args? This may depend on the umask
27-
)
9+
/**
10+
* Classes and predicates for modelling the `File` module from the standard
11+
* library.
12+
*/
13+
private module File {
14+
private class FileModuleReader extends FileSystemReadAccess::Range, DataFlow::CallNode {
15+
FileModuleReader() { this = API::getTopLevelMember("File").getAMethodCall(["new", "open"]) }
16+
17+
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
18+
19+
override DataFlow::Node getADataNode() { result = this }
2820
}
2921

30-
MethodCall getCall() { result = call.asExpr().getExpr() }
31-
}
32-
*/
22+
private class FileModuleFilenameSource extends FileNameSource {
23+
FileModuleFilenameSource() {
24+
// Class methods
25+
this =
26+
API::getTopLevelMember("File")
27+
.getAMethodCall([
28+
"absolute_path", "basename", "expand_path", "join", "path", "readlink",
29+
"realdirpath", "realpath"
30+
])
31+
}
32+
}
3333

34+
private class FileModulePermissionModification extends FileSystemPermissionModification::Range,
35+
DataFlow::CallNode {
36+
private DataFlow::Node permissionArg;
3437

35-
class StdLibFileNameSource extends FileNameSource {
36-
StdLibFileNameSource() {
37-
this = API::getTopLevelMember("File").getAMethodCall(["join", "path", "to_path", "readlink"])
38+
FileModulePermissionModification() {
39+
exists(string methodName | this = API::getTopLevelMember("File").getAMethodCall(methodName) |
40+
methodName in ["chmod", "lchmod"] and permissionArg = this.getArgument(0)
41+
or
42+
methodName = "mkfifo" and permissionArg = this.getArgument(1)
43+
or
44+
methodName in ["new", "open"] and permissionArg = this.getArgument(2)
45+
// TODO: defaults for optional args? This may depend on the umask
46+
)
47+
}
3848

49+
override DataFlow::Node getAPermissionNode() { result = permissionArg }
3950
}
4051
}
4152

42-
/**
43-
* Classes and predicates for modelling the `File` module from the standard
44-
* library.
45-
*/
46-
private module File {
53+
private module FileUtils {
54+
private class FileUtilsFilenameSource extends FileNameSource {
55+
FileUtilsFilenameSource() {
56+
// Note that many methods in FileUtils accept a `noop` option that will
57+
// perform a dry run of the command. This means that, for instance, `rm`
58+
// and similar methods may not actually delete/unlink a file.
59+
this =
60+
API::getTopLevelMember("FileUtils")
61+
.getAMethodCall([
62+
"chmod", "chmod_R", "chown", "chown_R", "getwd", "makedirs", "mkdir", "mkdir_p",
63+
"mkpath", "remove", "remove_dir", "remove_entry", "rm", "rm_f", "rm_r", "rm_rf",
64+
"rmdir", "rmtree", "safe_unlink", "touch"
65+
])
66+
}
67+
}
68+
69+
private class FileUtilsPermissionModification extends FileSystemPermissionModification::Range,
70+
DataFlow::CallNode {
71+
private DataFlow::Node permissionArg;
4772

48-
class FileModuleReader extends FileSystemReadAccess, DataFlow::CallNode {
49-
FileModuleReader() {
50-
this = API::getTopLevelMember("File").getAMethodCall(["new", "open"])
73+
FileUtilsPermissionModification() {
74+
exists(string methodName |
75+
this = API::getTopLevelMember("FileUtils").getAMethodCall(methodName)
76+
|
77+
methodName in ["chmod", "chmod_R"] and permissionArg = this.getArgument(0)
78+
or
79+
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
80+
permissionArg = this.getKeywordArgument("mode")
81+
// TODO: defaults for optional args? This may depend on the umask
82+
)
5183
}
84+
85+
override DataFlow::Node getAPermissionNode() { result = permissionArg }
5286
}
87+
}
5388

54-
}
89+
private module IO { }

ql/src/queries/security/cwe-732/WeakFilePermissions.ql

Lines changed: 8 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import ruby
14+
import codeql.ruby.Concepts
1415
import codeql.ruby.DataFlow
1516
import DataFlow::PathGraph
1617
import codeql.ruby.ApiGraphs
@@ -43,42 +44,21 @@ class PermissivePermissionsExpr extends Expr {
4344
}
4445
}
4546

46-
/** A permissions argument of a call to a File/FileUtils method that may modify file permissions */
47-
class PermissionArgument extends DataFlow::Node {
48-
private DataFlow::CallNode call;
49-
50-
PermissionArgument() {
51-
exists(string methodName |
52-
call = API::getTopLevelMember(["File", "FileUtils"]).getAMethodCall(methodName)
53-
|
54-
methodName in ["chmod", "chmod_R", "lchmod"] and this = call.getArgument(0)
55-
or
56-
methodName = "mkfifo" and this = call.getArgument(1)
57-
or
58-
methodName in ["new", "open"] and this = call.getArgument(2)
59-
or
60-
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
61-
this = call.getKeywordArgument("mode")
62-
// TODO: defaults for optional args? This may depend on the umask
63-
)
64-
}
65-
66-
MethodCall getCall() { result = call.asExpr().getExpr() }
67-
}
68-
6947
class PermissivePermissionsConfig extends DataFlow::Configuration {
7048
PermissivePermissionsConfig() { this = "PermissivePermissionsConfig" }
7149

7250
override predicate isSource(DataFlow::Node source) {
7351
exists(PermissivePermissionsExpr ppe | source.asExpr().getExpr() = ppe)
7452
}
7553

76-
override predicate isSink(DataFlow::Node sink) { sink instanceof PermissionArgument }
54+
override predicate isSink(DataFlow::Node sink) {
55+
exists(FileSystemPermissionModification mod | mod.getAPermissionNode() = sink)
56+
}
7757
}
7858

7959
from
8060
DataFlow::PathNode source, DataFlow::PathNode sink, PermissivePermissionsConfig conf,
81-
PermissionArgument arg
82-
where conf.hasFlowPath(source, sink) and arg = sink.getNode()
83-
select source.getNode(), source, sink, "Overly permissive mask in $@ sets file to $@.",
84-
arg.getCall(), arg.getCall().toString(), source.getNode(), source.getNode().toString()
61+
FileSystemPermissionModification mod
62+
where conf.hasFlowPath(source, sink) and mod.getAPermissionNode() = sink.getNode()
63+
select source.getNode(), source, sink, "Overly permissive mask in $@ sets file to $@.", mod,
64+
mod.toString(), source.getNode(), source.getNode().toString()
Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
require "fileutils"
22

33
def run_chmod_1(filename)
4+
# BAD: sets file as world writable
45
FileUtils.chmod 0222, filename
6+
# BAD: sets file as world writable
57
FileUtils.chmod 0622, filename
8+
# BAD: sets file as world readable
69
FileUtils.chmod 0755, filename
10+
# BAD: sets file as world readable + writable
711
FileUtils.chmod 0777, filename
812
end
913

@@ -14,45 +18,56 @@ def chmod(mode, list, options = {} )
1418
end
1519

1620
def run_chmod_2(filename)
17-
foo = FileUtils
21+
foo = File
1822
bar = foo
19-
baz = Dummy
20-
# "safe"
23+
baz = DummyModule
24+
# GOOD: DummyModule is not a known class that performs file permission modifications
2125
baz.chmod 0755, filename
2226
baz = bar
23-
# unsafe
27+
# BAD: sets file as world readable
2428
baz.chmod 0755, filename
2529
end
2630

2731
def run_chmod_3(filename)
2832
# TODO: we currently miss this
2933
foo = FileUtils
3034
bar, baz = foo, 7
35+
# BAD: sets file as world readable
3136
bar.chmod 0755, filename
3237
end
3338

3439
def run_chmod_4(filename)
35-
# safe permissions
40+
# GOOD: no group/world access
3641
FileUtils.chmod 0700, filename
42+
# GOOD: group/world execute bit only
3743
FileUtils.chmod 0711, filename
44+
# GOOD: world execute bit only
3845
FileUtils.chmod 0701, filename
46+
# GOOD: group execute bit only
3947
FileUtils.chmod 0710, filename
4048
end
4149

4250
def run_chmod_5(filename)
4351
perm = 0777
52+
# BAD: sets world rwx
4453
FileUtils.chmod perm, filename
4554
perm2 = perm
55+
# BAD: sets world rwx
4656
FileUtils.chmod perm2, filename
4757

4858
perm = "u=wrx,g=rwx,o=x"
4959
perm2 = perm
60+
# BAD: sets group rwx
5061
FileUtils.chmod perm2, filename
62+
# BAD: sets file as world readable
5163
FileUtils.chmod "u=rwx,o+r", filename
64+
# GOOD: sets file as group/world unreadable
5265
FileUtils.chmod "u=rwx,go-r", filename
66+
# BAD: sets group/world as +rw
5367
FileUtils.chmod "a+rw", filename
5468
end
5569

5670
def run_chmod_R(filename)
57-
File.chmod_R 0755, filename
71+
# BAD: sets file as world readable
72+
FileUtils.chmod_R 0755, filename
5873
end

0 commit comments

Comments
 (0)