revamp weak file permissions query

This commit is contained in:
Alex Ford
2021-09-03 15:39:40 +01:00
parent 25300cb2b4
commit d1f2258d45
5 changed files with 210 additions and 118 deletions

View File

@@ -37,45 +37,107 @@ module SqlExecution {
}
/**
* A data flow node that performs a file system access (read, write, copy, permissions, stats, etc).
* A data flow node that performs a file system access, including reading and writing data,
* creating and deleting files and folders, checking and updating permissions, and so on.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `FileSystemAccess::Range` instead.
*/
abstract class FileSystemAccess extends DataFlow::Node {
class FileSystemAccess extends DataFlow::Node {
FileSystemAccess::Range range;
FileSystemAccess() { this = range }
/** Gets an argument to this file system access that is interpreted as a path. */
abstract DataFlow::Node getAPathArgument();
DataFlow::Node getAPathArgument() { result = range.getAPathArgument() }
}
/** Provides a class for modeling new file system access APIs. */
module FileSystemAccess {
/**
* Gets an argument to this file system access that is interpreted as a root folder
* in which the path arguments are constrained.
* A data-flow node that performs a file system access, including reading and writing data,
* creating and deleting files and folders, checking and updating permissions, and so on.
*
* In other words, if a root argument is provided, the underlying file access does its own
* sanitization to prevent the path arguments from traversing outside the root folder.
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `FileSystemAccess` instead.
*/
DataFlow::Node getRootPathArgument() { none() }
/**
* Holds if this file system access will reject paths containing upward navigation
* segments (`../`).
*
* `argument` should refer to the relevant path argument or root path argument.
*/
predicate isUpwardNavigationRejected(DataFlow::Node argument) { none() }
abstract class Range extends DataFlow::Node {
/** Gets an argument to this file system access that is interpreted as a path. */
abstract DataFlow::Node getAPathArgument();
}
}
/**
* A data flow node that reads data from the file system.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `FileSystemReadAccess::Range` instead.
*/
abstract class FileSystemReadAccess extends FileSystemAccess {
/** Gets a node that represents data from the file system. */
abstract DataFlow::Node getADataNode();
class FileSystemReadAccess extends FileSystemAccess {
override FileSystemReadAccess::Range range;
/**
* Gets a node that represents data read from the file system access.
*/
DataFlow::Node getADataNode() { result = range.getADataNode() }
}
/** Provides a class for modeling new file system reads. */
module FileSystemReadAccess {
/**
* A data flow node that reads data from the file system.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `FileSystemReadAccess` instead.
*/
abstract class Range extends FileSystemAccess::Range {
/**
* Gets a node that represents data read from the file system.
*/
abstract DataFlow::Node getADataNode();
}
}
/**
* A data flow node that sets the permissions for one or more files.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `FileSystemPermissionModification::Range` instead.
*/
class FileSystemPermissionModification extends DataFlow::Node {
FileSystemPermissionModification::Range range;
FileSystemPermissionModification() { this = range }
/**
* Gets an argument to this permission modification that is interpreted as a
* set of permissions.
*/
DataFlow::Node getAPermissionNode() { result = range.getAPermissionNode() }
}
/** Provides a class for modeling new file system permission modifications. */
module FileSystemPermissionModification {
/**
* A data-flow node that sets permissions for a one or more files.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `FileSystemPermissionModification` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets an argument to this permission modification that is interpreted as a
* set of permissions.
*/
abstract DataFlow::Node getAPermissionNode();
}
}
/**
* A data flow node that contains a file name or an array of file names from the local file system.
*/
abstract class FileNameSource extends DataFlow::Node { }
/**
* A data-flow node that escapes meta-characters, which could be used to prevent
* injection attacks.

View File

@@ -6,49 +6,84 @@ private import ruby
private import codeql.ruby.Concepts
private import codeql.ruby.ApiGraphs
/** A permissions argument of a call to a File/FileUtils method that may modify file permissions */
/*
class PermissionArgument extends DataFlow::Node {
private DataFlow::CallNode call;
PermissionArgument() {
exists(string methodName |
call = API::getTopLevelMember(["File", "FileUtils"]).getAMethodCall(methodName)
|
methodName in ["chmod", "chmod_R", "lchmod"] and this = call.getArgument(0)
or
methodName = "mkfifo" and this = call.getArgument(1)
or
methodName in ["new", "open"] and this = call.getArgument(2)
or
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
this = call.getKeywordArgument("mode")
// TODO: defaults for optional args? This may depend on the umask
)
}
MethodCall getCall() { result = call.asExpr().getExpr() }
}
*/
class StdLibFileNameSource extends FileNameSource {
StdLibFileNameSource() {
this = API::getTopLevelMember("File").getAMethodCall(["join", "path", "to_path", "readlink"])
}
}
/**
* Classes and predicates for modelling the `File` module from the standard
* library.
*/
private module File {
private class FileModuleReader extends FileSystemReadAccess::Range, DataFlow::CallNode {
FileModuleReader() { this = API::getTopLevelMember("File").getAMethodCall(["new", "open"]) }
class FileModuleReader extends FileSystemReadAccess, DataFlow::CallNode {
FileModuleReader() {
this = API::getTopLevelMember("File").getAMethodCall(["new", "open"])
override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
override DataFlow::Node getADataNode() { result = this }
}
private class FileModuleFilenameSource extends FileNameSource {
FileModuleFilenameSource() {
// Class methods
this =
API::getTopLevelMember("File")
.getAMethodCall([
"absolute_path", "basename", "expand_path", "join", "path", "readlink",
"realdirpath", "realpath"
])
}
}
}
private class FileModulePermissionModification extends FileSystemPermissionModification::Range,
DataFlow::CallNode {
private DataFlow::Node permissionArg;
FileModulePermissionModification() {
exists(string methodName | this = API::getTopLevelMember("File").getAMethodCall(methodName) |
methodName in ["chmod", "lchmod"] and permissionArg = this.getArgument(0)
or
methodName = "mkfifo" and permissionArg = this.getArgument(1)
or
methodName in ["new", "open"] and permissionArg = this.getArgument(2)
// TODO: defaults for optional args? This may depend on the umask
)
}
override DataFlow::Node getAPermissionNode() { result = permissionArg }
}
}
private module FileUtils {
private class FileUtilsFilenameSource extends FileNameSource {
FileUtilsFilenameSource() {
// Note that many methods in FileUtils accept a `noop` option that will
// perform a dry run of the command. This means that, for instance, `rm`
// and similar methods may not actually delete/unlink a file.
this =
API::getTopLevelMember("FileUtils")
.getAMethodCall([
"chmod", "chmod_R", "chown", "chown_R", "getwd", "makedirs", "mkdir", "mkdir_p",
"mkpath", "remove", "remove_dir", "remove_entry", "rm", "rm_f", "rm_r", "rm_rf",
"rmdir", "rmtree", "safe_unlink", "touch"
])
}
}
private class FileUtilsPermissionModification extends FileSystemPermissionModification::Range,
DataFlow::CallNode {
private DataFlow::Node permissionArg;
FileUtilsPermissionModification() {
exists(string methodName |
this = API::getTopLevelMember("FileUtils").getAMethodCall(methodName)
|
methodName in ["chmod", "chmod_R"] and permissionArg = this.getArgument(0)
or
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
permissionArg = this.getKeywordArgument("mode")
// TODO: defaults for optional args? This may depend on the umask
)
}
override DataFlow::Node getAPermissionNode() { result = permissionArg }
}
}
private module IO { }

View File

@@ -11,6 +11,7 @@
*/
import ruby
import codeql.ruby.Concepts
import codeql.ruby.DataFlow
import DataFlow::PathGraph
import codeql.ruby.ApiGraphs
@@ -43,29 +44,6 @@ class PermissivePermissionsExpr extends Expr {
}
}
/** A permissions argument of a call to a File/FileUtils method that may modify file permissions */
class PermissionArgument extends DataFlow::Node {
private DataFlow::CallNode call;
PermissionArgument() {
exists(string methodName |
call = API::getTopLevelMember(["File", "FileUtils"]).getAMethodCall(methodName)
|
methodName in ["chmod", "chmod_R", "lchmod"] and this = call.getArgument(0)
or
methodName = "mkfifo" and this = call.getArgument(1)
or
methodName in ["new", "open"] and this = call.getArgument(2)
or
methodName in ["install", "makedirs", "mkdir", "mkdir_p", "mkpath"] and
this = call.getKeywordArgument("mode")
// TODO: defaults for optional args? This may depend on the umask
)
}
MethodCall getCall() { result = call.asExpr().getExpr() }
}
class PermissivePermissionsConfig extends DataFlow::Configuration {
PermissivePermissionsConfig() { this = "PermissivePermissionsConfig" }
@@ -73,12 +51,14 @@ class PermissivePermissionsConfig extends DataFlow::Configuration {
exists(PermissivePermissionsExpr ppe | source.asExpr().getExpr() = ppe)
}
override predicate isSink(DataFlow::Node sink) { sink instanceof PermissionArgument }
override predicate isSink(DataFlow::Node sink) {
exists(FileSystemPermissionModification mod | mod.getAPermissionNode() = sink)
}
}
from
DataFlow::PathNode source, DataFlow::PathNode sink, PermissivePermissionsConfig conf,
PermissionArgument arg
where conf.hasFlowPath(source, sink) and arg = sink.getNode()
select source.getNode(), source, sink, "Overly permissive mask in $@ sets file to $@.",
arg.getCall(), arg.getCall().toString(), source.getNode(), source.getNode().toString()
FileSystemPermissionModification mod
where conf.hasFlowPath(source, sink) and mod.getAPermissionNode() = sink.getNode()
select source.getNode(), source, sink, "Overly permissive mask in $@ sets file to $@.", mod,
mod.toString(), source.getNode(), source.getNode().toString()

View File

@@ -1,9 +1,13 @@
require "fileutils"
def run_chmod_1(filename)
# BAD: sets file as world writable
FileUtils.chmod 0222, filename
# BAD: sets file as world writable
FileUtils.chmod 0622, filename
# BAD: sets file as world readable
FileUtils.chmod 0755, filename
# BAD: sets file as world readable + writable
FileUtils.chmod 0777, filename
end
@@ -14,13 +18,13 @@ module DummyModule
end
def run_chmod_2(filename)
foo = FileUtils
foo = File
bar = foo
baz = Dummy
# "safe"
baz = DummyModule
# GOOD: DummyModule is not a known class that performs file permission modifications
baz.chmod 0755, filename
baz = bar
# unsafe
# BAD: sets file as world readable
baz.chmod 0755, filename
end
@@ -28,31 +32,42 @@ def run_chmod_3(filename)
# TODO: we currently miss this
foo = FileUtils
bar, baz = foo, 7
# BAD: sets file as world readable
bar.chmod 0755, filename
end
def run_chmod_4(filename)
# safe permissions
# GOOD: no group/world access
FileUtils.chmod 0700, filename
# GOOD: group/world execute bit only
FileUtils.chmod 0711, filename
# GOOD: world execute bit only
FileUtils.chmod 0701, filename
# GOOD: group execute bit only
FileUtils.chmod 0710, filename
end
def run_chmod_5(filename)
perm = 0777
# BAD: sets world rwx
FileUtils.chmod perm, filename
perm2 = perm
# BAD: sets world rwx
FileUtils.chmod perm2, filename
perm = "u=wrx,g=rwx,o=x"
perm2 = perm
# BAD: sets group rwx
FileUtils.chmod perm2, filename
# BAD: sets file as world readable
FileUtils.chmod "u=rwx,o+r", filename
# GOOD: sets file as group/world unreadable
FileUtils.chmod "u=rwx,go-r", filename
# BAD: sets group/world as +rw
FileUtils.chmod "a+rw", filename
end
def run_chmod_R(filename)
File.chmod_R 0755, filename
# BAD: sets file as world readable
FileUtils.chmod_R 0755, filename
end

View File

@@ -1,31 +1,31 @@
edges
| FilePermissions.rb:43:10:43:13 | 0777 : | FilePermissions.rb:44:19:44:22 | perm |
| FilePermissions.rb:43:10:43:13 | 0777 : | FilePermissions.rb:46:19:46:23 | perm2 |
| FilePermissions.rb:48:10:48:26 | "u=wrx,g=rwx,o=x" : | FilePermissions.rb:50:19:50:23 | perm2 |
| FilePermissions.rb:51:10:51:13 | 0777 : | FilePermissions.rb:53:19:53:22 | perm |
| FilePermissions.rb:51:10:51:13 | 0777 : | FilePermissions.rb:56:19:56:23 | perm2 |
| FilePermissions.rb:58:10:58:26 | "u=wrx,g=rwx,o=x" : | FilePermissions.rb:61:19:61:23 | perm2 |
nodes
| FilePermissions.rb:4:19:4:22 | 0222 | semmle.label | 0222 |
| FilePermissions.rb:5:19:5:22 | 0622 | semmle.label | 0622 |
| FilePermissions.rb:6:19:6:22 | 0755 | semmle.label | 0755 |
| FilePermissions.rb:7:19:7:22 | 0777 | semmle.label | 0777 |
| FilePermissions.rb:24:13:24:16 | 0755 | semmle.label | 0755 |
| FilePermissions.rb:43:10:43:13 | 0777 : | semmle.label | 0777 : |
| FilePermissions.rb:44:19:44:22 | perm | semmle.label | perm |
| FilePermissions.rb:46:19:46:23 | perm2 | semmle.label | perm2 |
| FilePermissions.rb:48:10:48:26 | "u=wrx,g=rwx,o=x" : | semmle.label | "u=wrx,g=rwx,o=x" : |
| FilePermissions.rb:50:19:50:23 | perm2 | semmle.label | perm2 |
| FilePermissions.rb:51:19:51:29 | "u=rwx,o+r" | semmle.label | "u=rwx,o+r" |
| FilePermissions.rb:53:19:53:24 | "a+rw" | semmle.label | "a+rw" |
| FilePermissions.rb:57:16:57:19 | 0755 | semmle.label | 0755 |
| FilePermissions.rb:5:19:5:22 | 0222 | semmle.label | 0222 |
| FilePermissions.rb:7:19:7:22 | 0622 | semmle.label | 0622 |
| FilePermissions.rb:9:19:9:22 | 0755 | semmle.label | 0755 |
| FilePermissions.rb:11:19:11:22 | 0777 | semmle.label | 0777 |
| FilePermissions.rb:28:13:28:16 | 0755 | semmle.label | 0755 |
| FilePermissions.rb:51:10:51:13 | 0777 : | semmle.label | 0777 : |
| FilePermissions.rb:53:19:53:22 | perm | semmle.label | perm |
| FilePermissions.rb:56:19:56:23 | perm2 | semmle.label | perm2 |
| FilePermissions.rb:58:10:58:26 | "u=wrx,g=rwx,o=x" : | semmle.label | "u=wrx,g=rwx,o=x" : |
| FilePermissions.rb:61:19:61:23 | perm2 | semmle.label | perm2 |
| FilePermissions.rb:63:19:63:29 | "u=rwx,o+r" | semmle.label | "u=rwx,o+r" |
| FilePermissions.rb:67:19:67:24 | "a+rw" | semmle.label | "a+rw" |
| FilePermissions.rb:72:21:72:24 | 0755 | semmle.label | 0755 |
subpaths
#select
| FilePermissions.rb:4:19:4:22 | 0222 | FilePermissions.rb:4:19:4:22 | 0222 | FilePermissions.rb:4:19:4:22 | 0222 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:4:3:4:32 | call to chmod | call to chmod | FilePermissions.rb:4:19:4:22 | 0222 | 0222 |
| FilePermissions.rb:5:19:5:22 | 0622 | FilePermissions.rb:5:19:5:22 | 0622 | FilePermissions.rb:5:19:5:22 | 0622 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:5:3:5:32 | call to chmod | call to chmod | FilePermissions.rb:5:19:5:22 | 0622 | 0622 |
| FilePermissions.rb:6:19:6:22 | 0755 | FilePermissions.rb:6:19:6:22 | 0755 | FilePermissions.rb:6:19:6:22 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:6:3:6:32 | call to chmod | call to chmod | FilePermissions.rb:6:19:6:22 | 0755 | 0755 |
| FilePermissions.rb:7:19:7:22 | 0777 | FilePermissions.rb:7:19:7:22 | 0777 | FilePermissions.rb:7:19:7:22 | 0777 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:7:3:7:32 | call to chmod | call to chmod | FilePermissions.rb:7:19:7:22 | 0777 | 0777 |
| FilePermissions.rb:24:13:24:16 | 0755 | FilePermissions.rb:24:13:24:16 | 0755 | FilePermissions.rb:24:13:24:16 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:24:3:24:26 | call to chmod | call to chmod | FilePermissions.rb:24:13:24:16 | 0755 | 0755 |
| FilePermissions.rb:43:10:43:13 | 0777 | FilePermissions.rb:43:10:43:13 | 0777 : | FilePermissions.rb:44:19:44:22 | perm | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:44:3:44:32 | call to chmod | call to chmod | FilePermissions.rb:43:10:43:13 | 0777 | 0777 |
| FilePermissions.rb:43:10:43:13 | 0777 | FilePermissions.rb:43:10:43:13 | 0777 : | FilePermissions.rb:46:19:46:23 | perm2 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:46:3:46:33 | call to chmod | call to chmod | FilePermissions.rb:43:10:43:13 | 0777 | 0777 |
| FilePermissions.rb:48:10:48:26 | "u=wrx,g=rwx,o=x" | FilePermissions.rb:48:10:48:26 | "u=wrx,g=rwx,o=x" : | FilePermissions.rb:50:19:50:23 | perm2 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:50:3:50:33 | call to chmod | call to chmod | FilePermissions.rb:48:10:48:26 | "u=wrx,g=rwx,o=x" | "u=wrx,g=rwx,o=x" |
| FilePermissions.rb:51:19:51:29 | "u=rwx,o+r" | FilePermissions.rb:51:19:51:29 | "u=rwx,o+r" | FilePermissions.rb:51:19:51:29 | "u=rwx,o+r" | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:51:3:51:39 | call to chmod | call to chmod | FilePermissions.rb:51:19:51:29 | "u=rwx,o+r" | "u=rwx,o+r" |
| FilePermissions.rb:53:19:53:24 | "a+rw" | FilePermissions.rb:53:19:53:24 | "a+rw" | FilePermissions.rb:53:19:53:24 | "a+rw" | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:53:3:53:34 | call to chmod | call to chmod | FilePermissions.rb:53:19:53:24 | "a+rw" | "a+rw" |
| FilePermissions.rb:57:16:57:19 | 0755 | FilePermissions.rb:57:16:57:19 | 0755 | FilePermissions.rb:57:16:57:19 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:57:3:57:29 | call to chmod_R | call to chmod_R | FilePermissions.rb:57:16:57:19 | 0755 | 0755 |
| FilePermissions.rb:5:19:5:22 | 0222 | FilePermissions.rb:5:19:5:22 | 0222 | FilePermissions.rb:5:19:5:22 | 0222 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:5:3:5:32 | call to chmod | call to chmod | FilePermissions.rb:5:19:5:22 | 0222 | 0222 |
| FilePermissions.rb:7:19:7:22 | 0622 | FilePermissions.rb:7:19:7:22 | 0622 | FilePermissions.rb:7:19:7:22 | 0622 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:7:3:7:32 | call to chmod | call to chmod | FilePermissions.rb:7:19:7:22 | 0622 | 0622 |
| FilePermissions.rb:9:19:9:22 | 0755 | FilePermissions.rb:9:19:9:22 | 0755 | FilePermissions.rb:9:19:9:22 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:9:3:9:32 | call to chmod | call to chmod | FilePermissions.rb:9:19:9:22 | 0755 | 0755 |
| FilePermissions.rb:11:19:11:22 | 0777 | FilePermissions.rb:11:19:11:22 | 0777 | FilePermissions.rb:11:19:11:22 | 0777 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:11:3:11:32 | call to chmod | call to chmod | FilePermissions.rb:11:19:11:22 | 0777 | 0777 |
| FilePermissions.rb:28:13:28:16 | 0755 | FilePermissions.rb:28:13:28:16 | 0755 | FilePermissions.rb:28:13:28:16 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:28:3:28:26 | call to chmod | call to chmod | FilePermissions.rb:28:13:28:16 | 0755 | 0755 |
| FilePermissions.rb:51:10:51:13 | 0777 | FilePermissions.rb:51:10:51:13 | 0777 : | FilePermissions.rb:53:19:53:22 | perm | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:53:3:53:32 | call to chmod | call to chmod | FilePermissions.rb:51:10:51:13 | 0777 | 0777 |
| FilePermissions.rb:51:10:51:13 | 0777 | FilePermissions.rb:51:10:51:13 | 0777 : | FilePermissions.rb:56:19:56:23 | perm2 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:56:3:56:33 | call to chmod | call to chmod | FilePermissions.rb:51:10:51:13 | 0777 | 0777 |
| FilePermissions.rb:58:10:58:26 | "u=wrx,g=rwx,o=x" | FilePermissions.rb:58:10:58:26 | "u=wrx,g=rwx,o=x" : | FilePermissions.rb:61:19:61:23 | perm2 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:61:3:61:33 | call to chmod | call to chmod | FilePermissions.rb:58:10:58:26 | "u=wrx,g=rwx,o=x" | "u=wrx,g=rwx,o=x" |
| FilePermissions.rb:63:19:63:29 | "u=rwx,o+r" | FilePermissions.rb:63:19:63:29 | "u=rwx,o+r" | FilePermissions.rb:63:19:63:29 | "u=rwx,o+r" | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:63:3:63:39 | call to chmod | call to chmod | FilePermissions.rb:63:19:63:29 | "u=rwx,o+r" | "u=rwx,o+r" |
| FilePermissions.rb:67:19:67:24 | "a+rw" | FilePermissions.rb:67:19:67:24 | "a+rw" | FilePermissions.rb:67:19:67:24 | "a+rw" | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:67:3:67:34 | call to chmod | call to chmod | FilePermissions.rb:67:19:67:24 | "a+rw" | "a+rw" |
| FilePermissions.rb:72:21:72:24 | 0755 | FilePermissions.rb:72:21:72:24 | 0755 | FilePermissions.rb:72:21:72:24 | 0755 | Overly permissive mask in $@ sets file to $@. | FilePermissions.rb:72:3:72:34 | call to chmod_R | call to chmod_R | FilePermissions.rb:72:21:72:24 | 0755 | 0755 |