Implement cookie write concepts and httponly query

This commit is contained in:
Joe Farebrother
2025-11-03 14:54:35 +00:00
parent 1c2d8bb70e
commit 7d76619bea
4 changed files with 233 additions and 0 deletions

View File

@@ -380,4 +380,96 @@ module Http {
/** Gets a node that is used in a check that is tested before this handler is run. */ /** Gets a node that is used in a check that is tested before this handler is run. */
predicate guardedBy(DataFlow::Node check) { super.guardedBy(check) } predicate guardedBy(DataFlow::Node check) { super.guardedBy(check) }
} }
/** Provides a class for modelling HTTP response cookie writes. */
module CookieWrite {
/**
* An write of an HTTP Cookie to an HTTP response.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HTTP::CookieWrite` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the name of the cookie written. */
abstract DataFlow::Node getName();
/** Gets the value of the cookie written. */
abstract DataFlow::Node getValue();
/** Gets the `Secure` attribute of the cookie written. */
abstract DataFlow::Node getSecure();
/** Gets the `HttpOnly` attribute of the cookie written. */
abstract DataFlow::Node getHttpOnly();
}
}
/**
* An write of an HTTP Cookie to an HTTP response.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `HTTP::CookieWrite::Range` instead.
*/
class CookieWrite extends DataFlow::Node instanceof CookieWrite::Range {
/** Gets the name of the cookie written. */
DataFlow::Node getName() { result = super.getName() }
/** Gets the value of the cookie written. */
DataFlow::Node getValue() { result = super.getValue() }
/** Gets the `Secure` attribute of the cookie written. */
DataFlow::Node getSecure() { result = super.getSecure() }
/** Gets the `HttpOnly` attribute of the cookie written. */
DataFlow::Node getHttpOnly() { result = super.getHttpOnly() }
}
/** Provides a class for modelling the options of an HTTP cookie. */
module CookieOptions {
/**
* An HTTP Cookie object.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HTTP::CookieOptions` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the node representing the cookie object for the options being set. */
abstract DataFlow::Node getCookieOutput();
/** Gets the name of the cookie represented. */
abstract DataFlow::Node getName();
/** Gets the value of the cookie represented. */
abstract DataFlow::Node getValue();
/** Gets the `Secure` attribute of the cookie represented. */
abstract DataFlow::Node getSecure();
/** Gets the `HttpOnly` attribute of the cookie represented. */
abstract DataFlow::Node getHttpOnly();
}
}
/**
* An HTTP Cookie.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `HTTP::CookieOptions::Range` instead.
*/
class CookieOptions extends DataFlow::Node instanceof CookieOptions::Range {
/** Gets the node representing the cookie object for the options being set. */
DataFlow::Node getCookieOutput() { result = super.getCookieOutput() }
/** Gets the name of the cookie represented. */
DataFlow::Node getName() { result = super.getName() }
/** Gets the value of the cookie represented. */
DataFlow::Node getValue() { result = super.getValue() }
/** Gets the `Secure` attribute of the cookie represented. */
DataFlow::Node getSecure() { result = super.getSecure() }
/** Gets the `HttpOnly` attribute of the cookie represented. */
DataFlow::Node getHttpOnly() { result = super.getHttpOnly() }
}
} }

View File

@@ -293,4 +293,38 @@ module NetHttp {
override DataFlow::Node getAPathArgument() { result = this.getArgument(2) } override DataFlow::Node getAPathArgument() { result = this.getArgument(2) }
} }
class CookieWrite extends Http::CookieWrite::Range, DataFlow::CallNode {
CookieWrite() { this.getTarget().hasQualifiedName(package("net/http", ""), "SetCookie") }
override DataFlow::Node getName() { result = this.getArgument(1) }
override DataFlow::Node getValue() { result = this.getArgument(1) }
override DataFlow::Node getSecure() { result = this.getArgument(1) }
override DataFlow::Node getHttpOnly() { result = this.getArgument(1) }
}
class CookieFieldWrite extends Http::CookieOptions::Range {
Write w;
Field f;
DataFlow::Node written;
string fieldName;
CookieFieldWrite() {
f.hasQualifiedName(package("net/http", ""), "Cookie", fieldName) and
w.writesField(this, f, written)
}
override DataFlow::Node getCookieOutput() { result = this }
override DataFlow::Node getName() { fieldName = "Name" and result = written }
override DataFlow::Node getValue() { fieldName = "Value" and result = written }
override DataFlow::Node getSecure() { fieldName = "Secure" and result = written }
override DataFlow::Node getHttpOnly() { fieldName = "HttpOnly" and result = written }
}
} }

View File

@@ -0,0 +1,79 @@
/** Provides classes and predicates for identifying HTTP cookies with insecure attributes. */
import go
import semmle.go.concepts.HTTP
import semmle.go.dataflow.DataFlow
/**
* Holds if the expression or its value has a sensitive name
*/
private predicate isSensitiveExpr(Expr expr, string val) {
(
val = expr.getStringValue() or
val = expr.(Name).getTarget().getName()
) and
val.regexpMatch("(?i).*(session|login|token|user|auth|credential).*") and
not val.regexpMatch("(?i).*(xsrf|csrf|forgery).*")
}
private module SensitiveCookieNameConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { isSensitiveExpr(source.asExpr(), _) }
predicate isSink(DataFlow::Node sink) { exists(Http::CookieWrite cw | sink = cw.getName()) }
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Http::CookieOptions co | co.getName() = pred and co.getCookieOutput() = succ)
}
}
/** Tracks flow from sensitive names to HTTP cookie writes. */
module SensitiveCookieNameFlow = DataFlow::Global<SensitiveCookieNameConfig>;
private module BooleanCookieSecureConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { exists(source.asExpr().getBoolValue()) }
predicate isSink(DataFlow::Node sink) { exists(Http::CookieWrite cw | sink = cw.getSecure()) }
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Http::CookieOptions co | co.getSecure() = pred and co.getCookieOutput() = succ)
}
}
/** Tracks flow from boolean expressions to the `Secure` attribute HTTP cookie writes. */
module BooleanCookieSecureFlow = DataFlow::Global<BooleanCookieSecureConfig>;
private module BooleanCookieHttpOnlyConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { exists(source.asExpr().getBoolValue()) }
predicate isSink(DataFlow::Node sink) { exists(Http::CookieWrite cw | sink = cw.getHttpOnly()) }
predicate isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(Http::CookieOptions co | co.getHttpOnly() = pred and co.getCookieOutput() = succ)
}
}
/** Tracks flow from boolean expressions to the `HttpOnly` attribute HTTP cookie writes. */
module BooleanCookieHttpOnlyFlow = DataFlow::Global<BooleanCookieHttpOnlyConfig>;
predicate isInsecureDefault(Http::CookieWrite cw) {
not BooleanCookieSecureFlow::flow(_, cw.getSecure())
}
predicate isNonHttpOnlyDefault(Http::CookieWrite cw) {
not BooleanCookieHttpOnlyFlow::flow(_, cw.getHttpOnly())
}
predicate isInsecureDirect(Http::CookieWrite cw, Expr boolFalse) {
BooleanCookieSecureFlow::flow(DataFlow::exprNode(boolFalse), cw.getSecure()) and
boolFalse.getBoolValue() = false
}
predicate isNonHttpOnlyDirect(Http::CookieWrite cw, Expr boolFalse) {
BooleanCookieHttpOnlyFlow::flow(DataFlow::exprNode(boolFalse), cw.getHttpOnly()) and
boolFalse.getBoolValue() = false
}
predicate isSensitiveCookie(Http::CookieWrite cw, Expr nameExpr, string name) {
SensitiveCookieNameFlow::flow(DataFlow::exprNode(nameExpr), cw.getName()) and
isSensitiveExpr(nameExpr, name)
}

View File

@@ -0,0 +1,28 @@
/**
* @name 'HttpOnly' attribute is not set to true
* @description Omitting the 'HttpOnly' attribute for security sensitive data allows
* malicious JavaScript to steal it in case of XSS vulnerability. Always set
* 'HttpOnly' to 'true' to authentication related cookie to make it
* not accessible by JavaScript.
* @kind problem
* @problem.severity warning
* @precision high
* @id go/cookie-httponly-not-set
* @tags security
* experimental
* external/cwe/cwe-1004
*/
import go
import semmle.go.security.SecureCookies
import semmle.go.concepts.HTTP
from Http::CookieWrite cw, Expr sensitiveNameExpr, string name
where
isSensitiveCookie(cw, sensitiveNameExpr, name) and
(
isNonHttpOnlyDefault(cw)
or
isNonHttpOnlyDirect(cw, _)
)
select cw, "Sensitive cookie $@ does not set HttpOnly to true", sensitiveNameExpr, name