CVE-2021-25981: Insufficient Session Expiration (CWE-613)
value: In Talkyard, regular versions v0.2021.20 through v0.2021.33 and dev versions v0.2021.20 through v0.2021.34, are vulnerable to Insufficient Session Expiration. This may allow an attacker to reuse the admin’s still-valid session token even when logged-out, to gain admin privileges, given the attacker is able to obtain that token (via other, hypothetical attacks)
CWE description: According to WASC, "Insufficient Session Expiration is when a web site permits an attacker to reuse old session credentials or session IDs for authorization."
CVSS v3 base score: 9.8
Published: 2022-01-03T07:15Z
Repository: https://github.com/debiki/talkyard
Language: Scala
Files changed: appsv/server/ed/server/http/PlainApiActions.scala, appsv/server/ed/server/pubsub/SubscriberController.scala, appsv/server/ed/server/security/package.scala, client/app-slim/Server.ts, client/app-slim/start-page.ts
Commit date: 2021-12-01 12:32:34 +0100
Commit message: Use fancy sids, not silly sids, by default.

Diff: diff --git a/appsv/server/ed/server/http/PlainApiActions.scala b/appsv/server/ed/server/http/PlainApiActions.scala
index 0ffb5eba2..e6f4128c2 100644
--- a/appsv/server/ed/server/http/PlainApiActions.scala
+++ b/appsv/server/ed/server/http/PlainApiActions.scala
@@ -547,14 +547,12 @@ class PlainApiActions(
       }
       else if (minAuthnStrength.fullSidRequired || staffOnly || adminOnly || superAdminOnly) {
         // Part 4 HttpOnly is required, but is missing.  Dupl code [btr_sid_part_4]
         assert(anyTySession.isDefined, "TyE04MWG245")
         assert(anyTySession.get.part4Absent, "TyE04MWG246")
-        val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
-        val onlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
-        val doUseFancySid = onlyFancySid || tryFancySid
-        if (doUseFancySid) {
+        val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
+        if (!useOldSid) {
           throwForbidden("TyEWEAKSID_",
                 s"Please log out and log in, to get a complete session id — \n" +
                 s"this endpoint, ${request.path}, requires the HttpOnly part of the session id")
         }
       }
diff --git a/appsv/server/ed/server/pubsub/SubscriberController.scala b/appsv/server/ed/server/pubsub/SubscriberController.scala
index 0cee9c09b..eb2ece553 100644
--- a/appsv/server/ed/server/pubsub/SubscriberController.scala
+++ b/appsv/server/ed/server/pubsub/SubscriberController.scala
@@ -160,14 +160,12 @@ class SubscriberController @Inject()(cc: ControllerComponents, tyCtx: EdContext)
 
     // For now, let's require part 4 HttpOnly to use WebSocket. And some time later
     // maybe reconsider, and e.g. allow live updates of embedded comments.
     if (anyTySession.exists(_.part4Absent)) {
       // Dupl code [btr_sid_part_4]
-      val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
-      val onlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
-      val doUseFancySid = onlyFancySid || tryFancySid
-      if (doUseFancySid) {
+      val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
+      if (!useOldSid) {
         UNTESTED
         throwForbidden("TyEWEAKSIDWS",
               s"Please log out and log in, to get a complete session id — \n" +
               s"WebSockets, ${request.path}, requires the HttpOnly part of the session id")
       }
diff --git a/appsv/server/ed/server/security/package.scala b/appsv/server/ed/server/security/package.scala
index a2fa90d45..b96a0f27c 100644
--- a/appsv/server/ed/server/security/package.scala
+++ b/appsv/server/ed/server/security/package.scala
@@ -786,12 +786,11 @@ class EdSecurity(globals: Globals) {
     AUDIT_LOG // Maybe log session id creation?
 
     val site = req.site
     val dao = req.dao
 
-    val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
-    val useFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
+    val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
 
     val ppt = dao.getParticipant(userId)
     throwForbiddenIf(ppt.exists(_.isGroup), "TyELGIGRP", "Cannot login as a group")  // [imp-groups]
 
     val settings = dao.getWholeSiteSettings()
@@ -802,11 +801,11 @@ class EdSecurity(globals: Globals) {
           maxAgeSecs = Some(expireIdleAfterSecs + XsrfAliveExtraSeconds))
 
     // New better sid  [btr_sid]
     // ----------------------------------------
 
-    if (tryFancySid || useFancySid) {
+    if (!useOldSid) {
       val (newSidCookies, session) = genAndSaveFancySid(req, patId = userId,
             expireIdleAfterSecs = expireIdleAfterSecs, dao.now(),
             dao.asInstanceOf[SessionSiteDaoMixin])
       val sidOk = SidOk(session.part1And2, ageInMillis = 0, Some(userId))
       return (sidOk, xsrfOk, xsrfCookie :: newSidCookies)
@@ -929,14 +928,13 @@ class EdSecurity(globals: Globals) {
         now: When,
         expireIdleAfterMillis: i64): CheckSidResult = {
 
     val hasFancySid = anyFancySidPart12Maybe3.isDefined || anyFancySidPart4.isDefined ||
           anyFancySidPart5.isDefined
-    val useOnlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
-    val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
+    val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
 
-    if (useOnlyFancySid || (tryFancySid && hasFancySid)) {
+    if (!useOldSid) {
       var result = checkFancySessionId(anyPart12Maybe3 = anyFancySidPart12Maybe3,
             anyPart4 = anyFancySidPart4, anyPart5 = anyFancySidPart5,
             dao, now, expireIdleAfterMillis = expireIdleAfterMillis)
       if (anySillySid.isDefined) {
         result = result.copy(
diff --git a/client/app-slim/Server.ts b/client/app-slim/Server.ts
index 960ee850f..39ca0272e 100644
--- a/client/app-slim/Server.ts
+++ b/client/app-slim/Server.ts
@@ -1157,11 +1157,11 @@ export function loginWithOneTimeSecret(oneTimeLoginSecret: string,
 /// cookies, not accessible here.)
 ///
 export function getCurSid12Maybe3(): St | N {  // [ts_authn_modl]
   const store: Store = debiki2.ReactStore.allData();
   const cookieName =
-          debiki2.store_isFeatFlagOn(store, 'ffUseNewSid') ? 'TyCoSid123' : 'dwCoSid';
+          !debiki2.store_isFeatFlagOn(store, 'ffUseOldSid') ? 'TyCoSid123' : 'dwCoSid';
   let sid = getSetCookie(cookieName);
   if (!sid) {
     // Cannot use store.me.mySidPart1 — we might not yet have loaded
     // the current user from the server; store.me might be stale.
     const typs: PageSession = getMainWin().typs;
diff --git a/client/app-slim/start-page.ts b/client/app-slim/start-page.ts
index 27a9bb955..276de3f7b 100644
--- a/client/app-slim/start-page.ts
+++ b/client/app-slim/start-page.ts
@@ -69,11 +69,12 @@ function handleLoginInOtherBrowserTab() {
   const store: Store = debiki2.ReactStore.allData();
   const me: Myself = store.me;
 
   // New style session id:  [btr_sid]
   // ------------------------------------------
-  if (debiki2.store_isFeatFlagOn(store, 'ffUseNewSid')) {
+  const useNewSid = !debiki2.store_isFeatFlagOn(store, 'ffUseOldSid');
+  if (useNewSid) {
     const sidParts123 = debiki2.Server.getCurSid12Maybe3();
     const stillTheSameSid = sidParts123
           ? me.mySidPart1 && sidParts123.startsWith(me.mySidPart1)
           : !me.mySidPart1;
     if (!stillTheSameSid) {

Vulnerable code:         val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
        val onlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
        val doUseFancySid = onlyFancySid || tryFancySid
        if (doUseFancySid) {
      val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
      val onlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
      val doUseFancySid = onlyFancySid || tryFancySid
      if (doUseFancySid) {
    val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
    val useFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
    if (tryFancySid || useFancySid) {
    val useOnlyFancySid = site.isFeatureEnabled("ffUseNewSid", globals.config.featureFlags)
    val tryFancySid = site.isFeatureEnabled("ffTryNewSid", globals.config.featureFlags)
    if (useOnlyFancySid || (tryFancySid && hasFancySid)) {
          debiki2.store_isFeatFlagOn(store, 'ffUseNewSid') ? 'TyCoSid123' : 'dwCoSid';
  if (debiki2.store_isFeatFlagOn(store, 'ffUseNewSid')) {
Fixed code:         val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
        if (!useOldSid) {
      val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
      if (!useOldSid) {
    val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
    if (!useOldSid) {
    val useOldSid = site.isFeatureEnabled("ffUseOldSid", globals.config.featureFlags)
    if (!useOldSid) {
          !debiki2.store_isFeatFlagOn(store, 'ffUseOldSid') ? 'TyCoSid123' : 'dwCoSid';
  const useNewSid = !debiki2.store_isFeatFlagOn(store, 'ffUseOldSid');
  if (useNewSid) {