Set the Secure and HttpOnly cookie flags from creation - #1136
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The flags now live where the cookies are made, which is where the gap was.
CookieUtils.newCookie(openam-shared) setsHttpOnlyat creation, so thecreateCookie→response.addCookiesinks (the logout deletions) carry it;newCookieCarriesTheHttpOnlyFlagTheDeploymentConfiguresis red at the base and green at the head.- The dead reflective
Cookie.class.getMethod("setHttpOnly")inaddCookieToResponseis gone. CDCServlet.java:675now deletes the auth-URL cookie by name, path/and its configured domain instead of re-adding the request's pathlessCookie.
issue (blocking): In the IdP discovery copy of addCookieToResponse, an HttpOnly cookie now goes through response.addCookie, which Tomcat validates, instead of the hand-written header, which it does not.
openam-federation/openam-idpdiscovery/src/main/java/com/sun/identity/saml2/idpdiscovery/CookieUtils.java:495, CookieWriterServlet.java:281, :306, :368
The standalone IdP discovery WAR has no SameSite key in libIDPDiscoveryConfig.properties, so getCookieSameSite() is null there and every HttpOnly=True deployment takes the new road into Tomcat's Rfc6265CookieProcessor. That processor throws IllegalArgumentException for a cookie domain .example.com ("An invalid domain"), which then fails on every _saml_idp write. It also throws for the value resetPreferredIDPCookie builds with encodeCookie=False: it joins IdPs with a literal space, so the second preferred IdP fails with "invalid character [32]". Measured against tomcat-embed-core 10.1.50 and 11.0.20. Response.addCookie has no handler, and doGetPost catches only IOException, so /saml2writer answers HTTP 500. At the base both configurations worked, because !isCookieHttpOnly() && … sent them down the addHeader road. Both choices are radio buttons or a text field in Configurator.jsp. The OpenAM server WAR is not affected, because it defaults to samesite=Lax.
if (!isCookieHttpOnly() && getCookieSameSite() == null) {
response.addCookie(cookie);
return;
}newCookie still sets the flag at creation, and the header road below already appends ;httponly. addCookieToResponseSetsHttpOnlyOnTheServletCookie (CookieUtilsTest.java:134) pins the routing this reverts and turns into a header row. The addCookie sink in this copy may then stay a CodeQL alert. Or: keep addCookie, and fall back to the header on IllegalArgumentException.
suggestion (non-blocking): The idpdiscovery suite has no HttpOnly-off row for addCookieToResponse, so making cookie.setHttpOnly(true) unconditional there stays green.
openam-federation/openam-idpdiscovery/src/test/java/com/sun/identity/saml2/idpdiscovery/CookieUtilsTest.java:134, :149
addCookieToResponse is called only under (false, true, null) and (true, true, "Lax"). The openam-shared suite has addCookieToResponseLeavesHttpOnlyAloneWhenNotConfigured, but this copy has no equivalent. The mutant is not equivalent: with httponly unset, CookieWriterServlet:281 would send _saml_idp with HttpOnly.
@Test
public void addCookieToResponseLeavesHttpOnlyAloneWhenNotConfigured() {
withCookieSettings(false, false, null, () -> {
HttpServletResponse response = mock(HttpServletResponse.class);
Cookie cookie = new Cookie("_saml_idp", "aWRw");
CookieUtils.addCookieToResponse(response, cookie);
assertFalse(cookie.isHttpOnly());
verify(response).addCookie(cookie);
});
}Pin: this row goes red against the unconditional setHttpOnly(true), and it stays the only addCookie row once the blocking fix above lands.
nitpick (non-blocking): The Verification paragraph's "Every new test was watched failing first" is true of 3 of the 9 new tests, and openam-shared has 5 new tests, not 6.
openam-shared/src/test/java/com/sun/identity/shared/encode/CookieUtilsTest.java:96, openam-federation/openam-idpdiscovery/src/test/java/com/sun/identity/saml2/idpdiscovery/CookieUtilsTest.java:117, :149
I ran the head tests against the base sources. In openam-shared only newCookieCarriesTheHttpOnlyFlagTheDeploymentConfigures goes red, because the base already set HttpOnly by reflection, which resolves under Jakarta, and already set Secure in newCookie. In idpdiscovery the two HttpOnly tests go red, and the Secure and SameSite-header tests pass. The fix is still pinned. The green rows cover unchanged or equivalent code, so the paragraph should name the red ones and call the rest characterization tests.
CookieUtils.newCookie set Secure from com.iplanet.am.cookie.secure but left HttpOnly to addCookieToResponse, which applied it by reflection - a Servlet 2.5 leftover - so a cookie handed straight to response.addCookie, as every logout deletion is, went out without it. Both flags are now set on the cookie as it is created, from the same properties, and addCookieToResponse calls Cookie.setHttpOnly directly. The IdP discovery copy of CookieUtils follows suit. The remaining cookies built by hand go the same way: the LoginServlet and CDCServlet deletions are built with the name, path and domain the cookie was set with instead of re-adding the request's Cookie object, the AMTESTCOOKIE probe goes through CookieUtils, and the Oracle Access Manager and SiteMinder sample adapters mark their session cookie Secure on an HTTPS request and HttpOnly.
The IdP discovery WAR has no SameSite setting, so after the previous commit every HttpOnly deployment sent the preferred-IdP cookie through response.addCookie. Tomcat's Rfc6265CookieProcessor rejects a cookie domain with a leading dot and the space-separated value of an unencoded preferred-IdP list, both reachable from the configurator, and /saml2writer answered 500. addCookieToResponse routes an HttpOnly cookie to the hand-built header again, as it did before; newCookie still sets both flags at creation. The servlet-API test for the HttpOnly case becomes a header test with a ".example.com" domain, and a row with HttpOnly off pins that the flag is not forced onto the servlet cookie.
8b1ecfa to
fe578af
Compare
|
issue (blocking) — confirmed and fixed by reverting the routing, as suggested first. I checked the claim against the source and ran the cookie through
On the CodeQL side, I don't expect this sink to come back. Its only alert, #29, is One thing for the record, outside this PR: at the base, suggestion — added as proposed: nitpick — the Verification paragraph is rewritten. Ten new tests, five per module. Against the base sources, two fail ( The branch is rebased onto the current master. |
|
The pre-existing |
maximthomas
left a comment
There was a problem hiding this comment.
praise: Round 2 fixes the idpdiscovery regression exactly where it was, and pins it.
addCookieToResponsein the idpdiscovery copy ofCookieUtilsis back to!isCookieHttpOnly() && getCookieSameSite() == null(:495), with a comment that says why.addCookieToResponseWritesTheHeaderItselfWhenHttpOnlyIsConfiguredasserts the hand-built header with a.example.comdomain, so round 1's gate turns it red.CookieUtils.newCookiesetsSecureandHttpOnlyat creation (openam-shared/src/main/java/com/sun/identity/shared/encode/CookieUtils.java:402-407), so a cookie handed straight toresponse.addCookiecarries them too.CDCServletclears the auth-URL cookie by the setter's name, path and domain (CDCServlet.java:675-676) instead of re-adding the request's cookie.
- aDomainWithoutALeadingDotIsSetAsItIs: a configured "example.com" is set as it is; stripping the first character of any domain now fails with "Domain=xample.com". - The addCookieToResponse comment and the OpenIdentityPlatform#1136 HttpOnly test no longer give the leading-dot domain or the unencoded list as the reason for the hand-built header, since newCookie produces neither; they name a domain the container still refuses, "example.com:8443".
The cookie cluster of the CodeQL medium triage:
java/insecure-cookie×19 andjava/sensitive-cookie-not-httponly×7.What the findings are about
OpenAM sets
SecureandHttpOnlyfrom configuration (com.iplanet.am.cookie.secure,com.sun.identity.cookie.httponly), which CodeQL cannot follow as such: it accepts a cookie as secure only when it flows fromsetSecure(true)orsetSecure(request.isSecure()), and as HttpOnly when it flows from asetHttpOnly(x)withxnot literallyfalse. Reading the sinks against that model turned up two real gaps rather than only a modelling mismatch:CookieUtils.newCookiesetSecurefrom the property but leftHttpOnlytoaddCookieToResponse, which applied it by reflection —Cookie.class.getMethod("setHttpOnly"), a Servlet 2.5 leftover that has been dead code since the move to Jakarta Servlet. 20 of the 26 sinks are cookies built bycreateCookie→newCookieand handed straight toresponse.addCookie(mostly the logout deletions), which therefore went out withoutHttpOnlyeven when it is configured.LoginServletandCDCServlet"reset" paths re-added the request's ownCookieobject with an empty value. A request cookie carries no path, domain or flags, so the deletion the browser received did not match the cookie that had been set with path/(and, for CDC, the auth-URL cookie domain).The change
CookieUtils.newCookiesets both flags on the cookie as it is created —if (isCookieSecure()) setSecure(true),if (isCookieHttpOnly()) setHttpOnly(true)— so every cookie the helper builds carries them whatever path adds it to the response.addCookieToResponsecallsCookie.setHttpOnlydirectly; the hand-builtSet-Cookieheader is kept for the SameSite case only, which the servletCookiecannot express. The IdP discovery war's copy ofCookieUtilssets both flags innewCookiethe same way, but itsaddCookieToResponsekeeps writing an HttpOnly cookie as a hand-built header: that WAR has no SameSite setting, and its configurator lets a deployment choose a cookie domain such as.example.comor an unencoded, space-separated preferred-IdP value, both of which Tomcat'sRfc6265CookieProcessorrejects inresponse.addCookie.LoginServlet: the host-only deletion is built withAuthUtils.createCookie(name, "", null)+maxAge 0, the same way the per-domain deletions next to it already are; theAMTESTCOOKIEprobe goes throughCookieUtils(only the server reads it back).CDCServlet: the auth-URL cookie is cleared by name, path/and its configured domain instead of re-adding the request object.src/main/integrations, not part of the build but scanned underbuild-mode: none) set their session cookieSecureon an HTTPS request andHttpOnly; their agents read the cookie from the request header, not from script.No configuration semantics change: a deployment that sets neither property gets exactly the cookies it got before, minus nothing.
Verification
Ten new tests, five in
openam-sharedand five inopenam-idpdiscovery. Run against the base sources, two of them fail —newCookieCarriesTheHttpOnlyFlagTheDeploymentConfiguresin each module (expected [true] but found [false]) — and pin the fix. The other eight pass at the base too: they are characterization tests of the routing and of theSecureflag, which the base already handled. The idpdiscovery HttpOnly-off row does go red against an unconditionalsetHttpOnly(true)inaddCookieToResponse.openam-shared1242 tests,openam-idpdiscovery13 — 0 failures, on the current master;openam-core2130 andopenam-federation-library177 — 0 failures,OpenFMandopenam-server-auth-uicompile, on the first round's base. TheLoginServlet/CDCServletwiring has no unit test (no servlet harness in those modules), and the two adapters cannot be compiled without the vendor SDKs.Expected effect on the scan
All 19
insecure-cookieand 7sensitive-cookie-not-httponlyalerts should close: the cookies now flow from a literalsetSecure(true)/setHttpOnly(true)innewCookie. Anything left after this PR's scan is a sink that receives a cookie built outsidenewCookie, to be looked at individually.