Skip to content

Conversation

@joefarebrother
Copy link
Contributor

Promotes go/cookie-httponly-not-set from experimental, and adds go/cookie-secure-not-set query.

@github-actions github-actions bot added the Go label Nov 5, 2025
@joefarebrother joefarebrother force-pushed the go-insecure-cookie branch 2 times, most recently from d00f670 to 2805a60 Compare November 10, 2025 09:31
@github-actions
Copy link
Contributor

github-actions bot commented Nov 10, 2025

QHelp previews:

go/ql/src/Security/CWE-1004/CookieWithoutHttpOnly.qhelp

Cookie 'HttpOnly' attribute is not set to true

Cookies without the HttpOnly flag set are accessible to client-side scripts such as JavaScript running in the same origin. In case of a Cross-Site Scripting (XSS) vulnerability, the cookie can be stolen by a malicious script. If a sensitive cookie does not need to be accessed directly by client-side JS, the HttpOnly flag should be set.

Recommendation

Set the HttpOnly flag to true for authentication cookies to ensure they are not accessible to client-side scripts.

Example

In the following example, in the case marked BAD, the HttpOnly flag is not set, so the default value of false is used. In the case marked GOOD, the HttpOnly flag is set to true.

package main

import (
	"net/http"
)

func handlerBad(w http.ResponseWriter, r *http.Request) {
	c := http.Cookie{
		Name:  "session",
		Value: "secret",
	}
	http.SetCookie(w, &c) // BAD: The HttpOnly flag is set to false by default.
}

func handlerGood(w http.ResponseWriter, r *http.Request) {
	c := http.Cookie{
		Name:     "session",
		Value:    "secret",
		HttpOnly: true,
	}
	http.SetCookie(w, &c) // GOOD: The HttpOnly flag is set to true.
}

References

go/ql/src/Security/CWE-614/CookieWithoutSecure.qhelp

Cookie 'Secure' attribute is not set to true

Cookies without the Secure flag set may be transmitted using HTTP instead of HTTPS. This leaves them vulnerable to being read by a third party attacker. If a sensitive cookie such as a session key is intercepted this way, it would allow the attacker to perform actions on a user's behalf.

Recommendation

Set the Secure flag to true to ensure cookies are only transmitted over secure HTTPS connections.

Example

In the following example, in the case marked BAD, the Secure flag is set to false by default. In the case marked GOOD, the Secure flag is set to true.

package main

import (
	"net/http"
)

func handlerBad(w http.ResponseWriter, r *http.Request) {
	c := http.Cookie{
		Name:  "session",
		Value: "secret",
	}
	http.SetCookie(w, &c) // BAD: The Secure flag is set to false by default.
}

func handlerGood(w http.ResponseWriter, r *http.Request) {
	c := http.Cookie{
		Name:   "session",
		Value:  "secret",
		Secure: true,
	}
	http.SetCookie(w, &c) // GOOD: The Secure flag is set to true.
}

References

@joefarebrother joefarebrother marked this pull request as ready for review November 10, 2025 10:35
@joefarebrother joefarebrother requested a review from a team as a code owner November 10, 2025 10:35
Copilot AI review requested due to automatic review settings November 10, 2025 10:35
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR promotes an experimental query for detecting cookies without the HttpOnly flag and introduces a new query for detecting cookies without the Secure flag. The changes include:

  • Migration of the CookieWithoutHttpOnly query from experimental to the main security query pack
  • Introduction of a new CookieWithoutSecure query
  • New shared library code (SecureCookies.qll) for modeling HTTP cookie security attributes
  • Updates to framework models for net/http and gin-gonic/gin

Reviewed Changes

Copilot reviewed 31 out of 42 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
go/ql/src/Security/CWE-1004/CookieWithoutHttpOnly.ql New query for detecting cookies without HttpOnly flag
go/ql/src/Security/CWE-614/CookieWithoutSecure.ql New query for detecting cookies without Secure flag
go/ql/lib/semmle/go/security/SecureCookies.qll Shared library for cookie security analysis
go/ql/lib/semmle/go/concepts/HTTP.qll Added CookieWrite and CookieOptions concepts
go/ql/lib/semmle/go/frameworks/NetHttp.qll Added cookie write models for net/http
go/ql/lib/semmle/go/frameworks/Gin.qll New framework models for gin-gonic/gin
go/ql/src/change-notes/2025-11-10-inseucre-cookie.md Change notes documenting the updates
go/ql/src/experimental/CWE-1004/* Removed experimental query files
go/ql/test/query-tests/Security/CWE-614/* Test files for CookieWithoutSecure
go/ql/test/query-tests/Security/CWE-1004/* Test files for CookieWithoutHttpOnly

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@owen-mc owen-mc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love the new concepts. I think they make the structure quite clean, and allow us to add other libraries easily in future.

I've only done a partial review so far. It will be easier to continue once these points have been addressed.

I note that you've removed modelling and tests for github.com/gorilla/sessions. Was that deliberate? It seems like a popular enough library, and it's only a bit of extra modelling and some tests that have already been written.

predicate guardedBy(DataFlow::Node check) { super.guardedBy(check) }
}

/** Provides a class for modeling HTTP response cookie writes. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Provides a class for modeling HTTP response cookie writes. */
/** Provides a class for modeling new HTTP response cookie write APIs. */

DataFlow::Node getHttpOnly() { result = super.getHttpOnly() }
}

/** Provides a class for modeling the options of an HTTP cookie. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Provides a class for modeling the options of an HTTP cookie. */
/** Provides a class for modeling new APIs for the options of an HTTP cookie. */

Comment on lines +310 to +311
Write w;
Field f;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than storing these as fields, they should be introduced in the characteristic predicate using exists, since they aren't referenced by any of the member predicates.

Comment on lines +463 to +473
/** 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() }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have I understood correctly that these won't always be defined? If so, add ", if any" to the end of the QLDocs for them. (Same in the Range class too.)

Comment on lines +428 to +430
module CookieOptions {
/**
* An HTTP Cookie object.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class name doesn't really match the QLDoc, and I think they're both wrong. It isn't a cookie object, or the options of a cookie. The only class extending it is NetHttp::CookieFieldWrite. I think it is a node which sets an option of a cookie, so maybe CookieOptionWrite?

import semmle.go.dataflow.DataFlow

/**
* Holds if the expression or its value has a sensitive name
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Holds if the expression or its value has a sensitive name
* Holds if the expression or its value has a sensitive name.

/**
* Holds if the expression or its value has a sensitive name
*/
private predicate isSensitiveExpr(Expr expr, string val) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The predicate name is confusing because in go/ql/lib/semmle/go/security/SensitiveActions.qll there is a class SensitiveExpr. A clearer name would be something like isSensitiveCookieName? But actually I think there is a better approach, which avoids having to come up with a clear name for it: make it an additional predicate in SensitiveCookieNameConfig with the signature isSource(Expr expr, string val) and then isSource(DataFlow::Node source) would just be isSource(source.asExpr(), _).

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()) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be cleaner with an additional predicate isSink(DataFlow::Node sink, Http::CookieWrite cw) { sink = cw.getName() } and then isSink(DataFlow::Node sink) { isSink(sink, _) }. Then you can use the additional predicate in isSensitiveCookie

* @kind problem
* @problem.severity warning
* @precision high
* @security-severity 5.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this be lowered to 4.0, as was recently done for java.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's confusing having predicates for two different queries in this one file (which is then only named after one of the predicates). Unless there is a good reason to keep them together, please put them in two separate files, named CookieWithoutSecure.qll and CookieWithoutHttpOnly.qll, so it's clear which query they relate to. I think there isn't any shared code - there probably was before you put things into concepts/HTTP.qll.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants