-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Swift: Add tests and develop command injection query #13906
Changes from all commits
048daa9
2664c30
8c2140b
7b9b96d
1c7d63a
348c45d
7177189
416b731
4c8accd
da34da7
a73354d
b2d3d46
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,50 +29,15 @@ class CommandInjectionAdditionalFlowStep extends Unit { | |
abstract predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo); | ||
} | ||
|
||
private class ProcessSink2 extends CommandInjectionSink instanceof DataFlow::Node { | ||
ProcessSink2() { | ||
exists(AssignExpr assign, ProcessHost s | | ||
assign.getDest() = s and | ||
this.asExpr() = assign.getSource() | ||
) | ||
or | ||
exists(AssignExpr assign, ProcessHost s, ArrayExpr a | | ||
assign.getDest() = s and | ||
a = assign.getSource() and | ||
this.asExpr() = a.getAnElement() | ||
) | ||
} | ||
} | ||
|
||
private class ProcessHost extends MemberRefExpr { | ||
ProcessHost() { this.getBase() instanceof ProcessRef } | ||
} | ||
|
||
/** An expression of type `Process`. */ | ||
private class ProcessRef extends Expr { | ||
ProcessRef() { | ||
this.getType() instanceof ProcessType or | ||
this.getType() = any(OptionalType t | t.getBaseType() instanceof ProcessType) | ||
} | ||
} | ||
|
||
/** The type `Process`. */ | ||
private class ProcessType extends NominalType { | ||
ProcessType() { this.getFullName() = "Process" } | ||
} | ||
|
||
/** | ||
* A `DataFlow::Node` that is written into a `Process` object. | ||
* An additional taint step for command injection vulnerabilities. | ||
*/ | ||
private class ProcessSink extends CommandInjectionSink instanceof DataFlow::Node { | ||
ProcessSink() { | ||
// any write into a class derived from `Process` is a sink. For | ||
// example in `Process.launchPath = sensitive` the post-update node corresponding | ||
// with `Process.launchPath` is a sink. | ||
exists(NominalType t, Expr e | | ||
t.getABaseType*().getUnderlyingType().getName() = "Process" and | ||
e.getFullyConverted() = this.asExpr() and | ||
e.getFullyConverted().getType() = t | ||
private class CommandInjectionArrayAdditionalFlowStep extends CommandInjectionAdditionalFlowStep { | ||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { | ||
// needed until we have proper content flow through arrays. | ||
exists(ArrayExpr arr | | ||
nodeFrom.asExpr() = arr.getAnElement() and | ||
nodeTo.asExpr() = arr | ||
) | ||
} | ||
} | ||
|
@@ -83,3 +48,24 @@ private class ProcessSink extends CommandInjectionSink instanceof DataFlow::Node | |
private class DefaultCommandInjectionSink extends CommandInjectionSink { | ||
DefaultCommandInjectionSink() { sinkNode(this, "command-injection") } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting pattern: specify domain-specific sinks/sources/summaries as CSV with a custom label. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do this for sinks in most queries now. In some cases (such as this one) we define the intended sinks via CSV, in other cases they're defined in QL but a CSV sink like this is provided purely as an extension point for users. Note that "command-injection" is the same string as is used in |
||
} | ||
|
||
private class CommandInjectionSinks extends SinkModelCsv { | ||
override predicate row(string row) { | ||
row = | ||
[ | ||
";Process;true;run(_:arguments:terminationHandler:);;;Argument[0..1];command-injection", | ||
";Process;true;launchedProcess(launchPath:arguments:);;;Argument[0..1];command-injection", | ||
";Process;true;arguments;;;PostUpdate;command-injection", | ||
";Process;true;currentDirectory;;;PostUpdate;command-injection", | ||
";Process;true;environment;;;PostUpdate;command-injection", | ||
";Process;true;executableURL;;;PostUpdate;command-injection", | ||
";Process;true;standardError;;;PostUpdate;command-injection", | ||
";Process;true;standardInput;;;PostUpdate;command-injection", | ||
";Process;true;standardOutput;;;PostUpdate;command-injection", | ||
";Process;true;currentDirectoryPath;;;PostUpdate;command-injection", | ||
";Process;true;launchPath;;;PostUpdate;command-injection", | ||
";NSUserScriptTask;true;init(url:);;;Argument[0];command-injection", | ||
";NSUserUnixTask;true;execute(withArguments:completionHandler:);;;Argument[0];command-injection", | ||
] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,10 @@ func validateCommand(_ command: String) -> String? { | |
return nil | ||
} | ||
|
||
var task = Process() | ||
task.launchPath = "/bin/bash" | ||
task.arguments = ["-c", validateCommand(userControlledString)] // GOOD | ||
if let validatedString = validateCommand(userControlledString) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would the query know that validatedString, to which userControlledString flows, is not tainted? I see in the test cases that this specific pattern is currently a false positive. Any ideas what it would take to fix it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think implementing a barrier for the qualifier of I'd like this work to cover all of the injection queries rather than being done ad-hoc. |
||
var task = Process() | ||
task.launchPath = "/bin/bash" | ||
task.arguments = ["-c", validatedString] // GOOD | ||
|
||
task.launch() | ||
task.launch() | ||
} |
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.
Interesting, didn't know you could have .OptionalSome in the CSV. Do you know how this string label "OptionalSome" is related to the actual implementation of the
[some:0]
ContentSet?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.
Yes, there's a special case in
parseContent
forOptionalSome
, it's just syntactic sugar forEnumElement[some:0]
(only cleaner and hopefully less error prone).