@@ -49,6 +49,12 @@ class RequestProcessor {
4949 private static final String KEY_FORM_POST = "form_post" ;
5050 private static final String KEY_MAX_AGE = "max_age" ;
5151
52+ // Upper bound for a valid session_expiry (Unix seconds). Anything at/above this is treated as
53+ // "no ceiling": it is almost certainly a milliseconds-since-epoch value emitted by mistake,
54+ // which would otherwise read as a date thousands of years out and silently disable enforcement.
55+ // Per the IPSIE Decision Log, reject anything >= 10,000,000,000.
56+ private static final long MAX_SESSION_EXPIRY_SECONDS = 10_000_000_000L ;
57+
5258 private final DomainProvider domainProvider ;
5359 private final String responseType ;
5460 private final String clientId ;
@@ -314,13 +320,16 @@ private Tokens getVerifiedTokens(HttpServletRequest request, HttpServletResponse
314320 * session ceiling on subsequent reads.
315321 * <p>
316322 * The claim is an integer Unix timestamp (seconds since epoch). When it is absent the tokens
317- * are returned unchanged (no ceiling). As a lockout guard, if the ceiling is already in the
318- * past relative to the token's {@code iat}, the login is rejected rather than producing an
319- * already-expired session.
323+ * are returned unchanged (no ceiling). The value is developer-controlled (it may be stamped by a
324+ * Post-Login Action), so it is validated rather than trusted: a non-numeric value, or one large
325+ * enough to be milliseconds-since-epoch ({@code >= 10_000_000_000}), is treated as "no ceiling"
326+ * rather than silently disabling enforcement with a date thousands of years out. As a lockout
327+ * guard, if the ceiling is already in the past relative to the token's {@code iat}, the login is
328+ * rejected rather than producing an already-expired session.
320329 *
321330 * @param tokens the merged tokens whose ID token is inspected.
322331 * @return the same tokens augmented with {@code sessionExpiresAt}, or {@code tokens} unchanged
323- * when no {@code session_expiry} claim is present.
332+ * when no usable {@code session_expiry} claim is present.
324333 * @throws IdentityVerificationException if {@code session_expiry <= iat}.
325334 */
326335 private Tokens withSessionExpiry (Tokens tokens ) throws IdentityVerificationException {
@@ -341,6 +350,13 @@ private Tokens withSessionExpiry(Tokens tokens) throws IdentityVerificationExcep
341350 return tokens ;
342351 }
343352
353+ // Range guard: reject milliseconds-since-epoch (or any absurdly large value). A value
354+ // accidentally emitted in milliseconds would read as a date ~thousands of years out and
355+ // silently switch off enforcement, so treat anything at/above this bound as "no ceiling".
356+ if (sessionExpiresAt >= MAX_SESSION_EXPIRY_SECONDS ) {
357+ return tokens ;
358+ }
359+
344360 // Lockout guard: a session that is already past its ceiling at login must not be persisted.
345361 Date issuedAt = decoded .getIssuedAt ();
346362 if (issuedAt != null && sessionExpiresAt <= Math .floorDiv (issuedAt .getTime (), 1000L )) {
0 commit comments