Python: Add SSRF queries

I've added 2 queries:

- one that detects full SSRF, where an attacker can control the full URL,
  which is always bad
- and one for partial SSRF, where an attacker can control parts of an
  URL (such as the path, query parameters, or fragment), which is not a
  big problem in many cases (but might still be exploitable)

full SSRF should run by default, and partial SSRF should not (but makes
it easy to see the other results).

Some elements of the full SSRF queries needs a bit more polishing, like
being able to detect `"https://" + user_input` is in fact controlling
the full URL.
This commit is contained in:
Rasmus Wriedt Larsen
2021-12-16 01:48:34 +01:00
parent 579de0c3f0
commit 1cc5e54357
12 changed files with 648 additions and 0 deletions

View File

@@ -0,0 +1,94 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* "Server-side request forgery"
* vulnerabilities, as well as extension points for adding your own.
*/
private import python
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.Concepts
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.BarrierGuards
/**
* Provides default sources, sinks and sanitizers for detecting
* "Server-side request forgery"
* vulnerabilities, as well as extension points for adding your own.
*/
module ServerSideRequestForgery {
/**
* A data flow source for "Server-side request forgery" vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }
/**
* A data flow sink for "Server-side request forgery" vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }
/**
* A sanitizer for "Server-side request forgery" vulnerabilities.
*/
abstract class Sanitizer extends DataFlow::Node { }
/**
* A sanitizer for "Server-side request forgery" vulnerabilities,
* that ensures the attacker does not have full control of the URL. (that is, might
* still be able to control path or query parameters).
*/
abstract class FullUrlControlSanitizer extends DataFlow::Node { }
/**
* A sanitizer guard for "Server-side request forgery" vulnerabilities.
*/
abstract class SanitizerGuard extends DataFlow::BarrierGuard { }
/**
* A source of remote user input, considered as a flow source.
*/
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
/** The URL of an HTTP request, considered as a sink. */
class HttpRequestUrlAsSink extends Sink {
HttpRequestUrlAsSink() {
exists(HTTP::Client::Request req | req.getAUrlPart() = this) and
// Since we find sinks inside stdlib, we need to exclude them manually. See
// comment for command injection sinks for more details.
not this.getScope().getEnclosingModule().getName() in ["http.client", "httplib"]
}
}
/**
* A comparison with a constant string, considered as a sanitizer-guard.
*/
class StringConstCompareAsSanitizerGuard extends SanitizerGuard, StringConstCompare { }
/**
* A string construction (concat, format, f-string) where the left side is not
* user-controlled.
*/
class StringConstructioneAsFullUrlControlSanitizer extends FullUrlControlSanitizer {
StringConstructioneAsFullUrlControlSanitizer() {
// string concat
exists(BinaryExprNode add |
add.getOp() instanceof Add and
add.getRight() = this.asCfgNode()
)
or
// % formatting
exists(BinaryExprNode fmt |
fmt.getOp() instanceof Mod and
fmt.getRight() = this.asCfgNode()
)
or
// arguments to a format call
exists(DataFlow::MethodCallNode call |
call.getMethodName() = "format" and
this in [call.getArg(_), call.getArgByName(_)]
)
or
// f-string
exists(Fstring fstring | fstring.getValue(any(int i | i > 0)) = this.asExpr())
}
}
}