Files
codeql/javascript/ql/src/Security/CWE-352/MissingCsrfMiddleware.ql
2022-12-14 12:30:15 +01:00

206 lines
7.6 KiB
Plaintext

/**
* @name Missing CSRF middleware
* @description Using cookies without CSRF protection may allow malicious websites to
* submit requests on behalf of the user.
* @kind problem
* @problem.severity error
* @security-severity 8.8
* @precision high
* @id js/missing-token-validation
* @tags security
* external/cwe/cwe-352
*/
import javascript
/** Gets a property name of `req` which refers to data usually derived from cookie data. */
string cookieProperty() { result = "session" or result = "cookies" or result = "user" }
/**
* Holds if `handler` uses cookies.
*/
predicate isRouteHandlerUsingCookies(Routing::RouteHandler handler) {
exists(DataFlow::PropRef value |
value = handler.getAParameter().ref().getAPropertyRead(cookieProperty()).getAPropertyReference() and
// Ignore accesses to values that are part of a CSRF or captcha check
not value.getPropertyName().regexpMatch("(?i).*(csrf|xsrf|captcha).*") and
// Ignore calls like `req.session.save()`
not value = any(DataFlow::InvokeNode call).getCalleeNode()
)
}
/**
* Holds if `route` is preceded by the cookie middleware `cookie`.
*
* A router handler following after cookie parsing is assumed to depend on
* cookies, and thus require CSRF protection.
*/
predicate hasCookieMiddleware(Routing::Node route, Http::CookieMiddlewareInstance cookie) {
route.isGuardedBy(cookie)
}
/**
* Gets an expression that creates a route handler which protects against CSRF attacks.
*
* Any route handler registered downstream from this type of route handler will
* be considered protected.
*
* For example:
* ```
* let csurf = require('csurf');
* let csrfProtector = csurf();
*
* app.post('/changePassword', csrfProtector, function (req, res) {
* // protected from CSRF
* })
* ```
*/
DataFlow::SourceNode csrfMiddlewareCreation() {
exists(DataFlow::SourceNode callee | result = callee.getACall() |
callee = DataFlow::moduleImport(["csurf", "tiny-csrf"])
or
callee = DataFlow::moduleImport("lusca") and
exists(result.(DataFlow::CallNode).getOptionArgument(0, "csrf"))
or
callee = DataFlow::moduleMember("lusca", "csrf")
or
callee = DataFlow::moduleMember("express", "csrf")
)
or
// Note: the 'fastify-csrf' plugin enables the 'fastify.csrfProtection' middleware to be installed.
// Simply having the plugin registered is not enough, so we look for the 'csrfProtection' middleware.
result = Fastify::server().getAPropertyRead("csrfProtection")
}
/** Holds if the given property has a name indicating that it refers to a CSRF token. */
pragma[nomagic]
private predicate isCsrfProperty(DataFlow::PropRef ref) {
ref.getPropertyName().regexpMatch("(?i).*(csrf|xsrf).*")
}
/**
* Gets a data flow node that flows to the base of a reference to `cookies`, `session`, or `user`,
* of which a property appears to be used in a CSRF token check.
*/
private DataFlow::SourceNode nodeLeadingToCsrfWriteOrCheck(DataFlow::TypeBackTracker t) {
t.start() and
exists(DataFlow::PropRef ref |
ref = result.getAPropertyRead(cookieProperty()).getAPropertyReference()
|
// Assignment to property with csrf/xsrf in the name
ref instanceof DataFlow::PropWrite and
isCsrfProperty(ref)
or
// Comparison where one of the properties has csrf/xsrf in the name
exists(EqualityTest test |
test.getAnOperand().flow().getALocalSource() = ref and
isCsrfProperty(test.getAnOperand().flow().getALocalSource())
)
or
// Comparison via a call, where one of the properties has csrf/xsrf in the name
exists(DataFlow::CallNode call |
call.getCalleeName().regexpMatch("(?i).*(check|verify|valid|equal).*") and
call.getAnArgument().getALocalSource() = ref and
isCsrfProperty(call.getAnArgument().getALocalSource())
)
)
or
exists(DataFlow::TypeBackTracker t2 | result = nodeLeadingToCsrfWriteOrCheck(t2).backtrack(t2, t))
}
/**
* Gets a route handler that sets an CSRF related cookie.
*/
private Routing::RouteHandler getAHandlerSettingCsrfCookie() {
exists(Http::CookieDefinition setCookie |
setCookie.getNameArgument().getStringValue().regexpMatch("(?i).*(csrf|xsrf).*") and
result = Routing::getRouteHandler(setCookie.getRouteHandler())
)
}
/**
* Holds if `handler` is protecting from CSRF.
* This is indicated either by the request parameter having a CSRF related write to a session variable.
* Or by the response parameter setting a CSRF related cookie.
*/
predicate isCsrfProtectionRouteHandler(Routing::RouteHandler handler) {
handler.getAParameter() = nodeLeadingToCsrfWriteOrCheck(DataFlow::TypeBackTracker::end())
or
handler = getAHandlerSettingCsrfCookie()
}
/** Gets a call to `passport.authenticate` */
API::CallNode passportAuthenticateCall() {
result = API::moduleImport("passport").getMember("authenticate").getACall()
}
/**
* Gets a call of form `passport.authenticate(..., { session: false })`, implying that the incoming
* request must carry its credentials rather than relying on cookies.
*
* In principle such routes should not be preceded by a cookie-parsing middleware, but to
* reduce noise we do not want to flag them.
*/
API::CallNode nonSessionBasedAuthMiddleware() {
result = passportAuthenticateCall() and
result.getParameter(1).getMember("session").asSink().mayHaveBooleanValue(false)
}
/**
* Gets a middleware node that performs authentication and is considered immune to Login CSRF.
*
* These are not considered CSRF protectors, but shouldn't themselves be flagged as being vulnerable
* to CSRF. In particular, if the middleware is wrapped by a router handler function, we want to avoid
* spuriously reporting the wrapper function.
*/
API::CallNode authMiddlewareImmuneToCsrf() {
result = passportAuthenticateCall() and
// The local strategy does not provide its own CSRF protection
not result.getArgument(0).getStringValue() = "local"
}
/**
* Gets an express route handler expression that is either a custom CSRF protection middleware,
* or a CSRF protecting library.
*/
Routing::Node getACsrfMiddleware() {
result = Routing::getNode(csrfMiddlewareCreation())
or
result = Routing::getNode(nonSessionBasedAuthMiddleware())
or
isCsrfProtectionRouteHandler(result)
}
/**
* Holds if the given route handler is protected by CSRF middleware.
*/
predicate hasCsrfMiddleware(Routing::RouteHandler handler) {
handler.isGuardedByNode(getACsrfMiddleware())
}
from
Routing::RouteSetup setup, Routing::Node setupArg, Routing::RouteHandler handler,
Http::CookieMiddlewareInstance cookie
where
// Require that the handler uses cookies and has cookie middleware.
//
// In practice, handlers that use cookies always have the cookie middleware or
// the handler wouldn't work. However, if we can't find the cookie middleware, it
// indicates that our middleware model is too incomplete, so in that case we
// don't trust it to detect the presence of CSRF middleware either.
setup.getAChild() = setupArg and
setupArg.getAChild*() = handler and
isRouteHandlerUsingCookies(handler) and
hasCookieMiddleware(handler, cookie) and
// Only flag the cookie parser registered first.
not hasCookieMiddleware(Routing::getNode(cookie), _) and
not hasCsrfMiddleware(handler) and
// Sometimes the CSRF protection comes later in the same route setup.
not setup.getAChild*() = getACsrfMiddleware() and
// Ignore auth routes that are immune to Login CSRF
not handler.getAChild*() = Routing::getNode(authMiddlewareImmuneToCsrf()) and
// Only warn for dangerous handlers, such as for POST and PUT.
setup.getOwnHttpMethod().isUnsafe()
select cookie, "This cookie middleware is serving a $@ without CSRF protection.", setupArg,
"request handler"