Skip to content
academia.sh

Lesson 09 / 17

Session Management Errors

Counting session decisions over a flow set: four of seven flows raise the session's authorization level, and two of those are fixation points because they do not renew the identifier; two of three invalidation flows delete no record server-side, so eight sessions continue to live on; a central session pattern brings this number down to two; when an eighth flow is added, the number of flows left uncovered in the distributed setup rises to five; and it is measured that the transfer flow, which never calls the pattern at all, is covered only by a pattern tied to the authorization version.

Published Updated

In this lesson

  • Counts the flows that generate a session identifier and flags the ones that do not renew it as fixation points
  • Measures the number of sessions that continue to live on when there is no server-side invalidation
  • Compares the flow coverage of distributed session decisions against a central pattern
Contents

The previous lesson measured the moment identity is established and counted that five of eight points leak user existence. Once authentication succeeds, a new object appears: the session identifier generated so identity does not have to be asked for again on subsequent requests. This identifier is generated once and afterward is changed, upgraded, or released by many flows.

What a session is and how it is stored was established in the Authentication and Authorization course. What is measured here is where the decisions are made. Every session-related decision is made inside a flow; as the number of flows grows, the number of decisions grows, and when each decision is written separately, some of them are written wrong. This lesson’s question is: how many flows touch the session identifier, how many touch it correctly, and how many are left uncovered.

Two Rules, Seven Flows

The flows that touch the session identifier fall into two types. An upgrade flow raises the session’s authorization level: an anonymous session becomes logged in, a logged-in session completes its second factor, a staff session gains administrator rights. The rule for this type is single — when the level changes, the session identifier is renewed. When it is not renewed, an identifier known before the level changed stays valid after the level changes too; this is called session fixation.

An invalidation flow, in turn, takes authorization back: logout, a password change, transferring a subscription to another person. The rule for this type is server-side — what has to be deleted is the record in the ledger. Deleting the cookie on the client side does nothing to the ledger; a client that holds on to the identifier can keep sending it.

The flow set and session ledger below are a model: there is no real session store, no real cookie, and no real subscriber.

// session/model.mjs — the subscriber portal's session flows and server-side session ledger
// (model): no real session store, no real cookie and no real subscriber.

// IE5: if a flow raises the session's authorization level, the session identifier must be
// renewed (expected: "renew"); if it takes authorization back, the affected sessions must be
// deleted server-side (expected: "single" for logout, "all" for the others). The `now` field
// is today's behavior.
export const FLOW = [
  { name: "portal-login",           type: "upgrade",    now: "renew" },
  { name: "second-factor-approval", type: "upgrade",    now: "in-place" },
  { name: "role-elevation",         type: "upgrade",    now: "in-place" },
  { name: "field-app-pairing",      type: "upgrade",    now: "renew" },
  { name: "logout",                 type: "invalidate", now: "single" },
  { name: "password-change",        type: "invalidate", now: "none" },
  { name: "subscription-transfer",  type: "invalidate", now: "none" },
];
export const expected = (a) =>
  a.type === "upgrade" ? "renew" : a.name === "logout" ? "single" : "all";

// IE6: the session ledger is server-side; an identifier resolves only while its record exists
// in the ledger. Deleting the cookie on the client side does not touch the ledger. When
// `versioned` is on, the record carries the subscriber's authorization version and becomes
// invalid when the version changes, without being deleted.
export function openLedger({ versioned = false } = {}) {
  const records = new Map(), version = new Map();
  let counter = 0;
  const versionOf = (subscriber) => version.get(subscriber) ?? 1;
  return {
    open(subscriber, level) {
      const k = `s-${++counter}`;
      records.set(k, { subscriber, level, version: versionOf(subscriber) });
      return k;
    },
    resolve(k) {
      const v = records.get(k);
      if (!v) return null;
      return versioned && v.version !== versionOf(v.subscriber) ? null : v;
    },
    updateInPlace(k, level) { const v = records.get(k); if (v) v.level = level; },
    remove: (k) => records.delete(k),
    removeAll(subscriber) {
      let n = 0;
      for (const [k, v] of records) if (v.subscriber === subscriber) { records.delete(k); n++; }
      return n;
    },
    bumpVersion(subscriber) { version.set(subscriber, versionOf(subscriber) + 1); },
    live(subscriber) {
      return [...records.keys()].filter((k) => this.resolve(k)?.subscriber === subscriber).length;
    },
  };
}
// session/measurement.mjs — how many flows generate a session identifier, how many fail to
// renew it, and how many sessions continue to live on when there is no invalidation.
import { FLOW, expected, openLedger } from "./model.mjs";
const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

// IE7: a fixation point is a flow where, after the upgrade, the old identifier still resolves
// to the upgraded level. The loop below actually runs this on the ledger.
const upgrades = FLOW.filter((a) => a.type === "upgrade");
const A = [-24, 10, 24, 20];
console.log(`1. ${upgrades.length} upgrade flows: what the old identifier resolves to after login`);
write(A, "flow", "now", "old identifier's level", "fixation point");
let fixation = 0;
for (const a of upgrades) {
  const d = openLedger();
  const old = d.open("a-4471", "anonymous");
  if (a.now === "renew") { d.remove(old); d.open("a-4471", "logged-in"); }
  else d.updateInPlace(old, "logged-in");
  const level = d.resolve(old)?.level ?? "not-resolved";
  if (level === "logged-in") fixation++;
  write(A, a.name, a.now, level, level === "logged-in" ? "YES" : "no");
}
console.log(`session-identifier-generating flow: ${upgrades.length}, not renewing: ${fixation}`);

// IE8: every subscriber has three live sessions opened from three separate endpoints; what is
// measured is the number of sessions remaining in the ledger after an invalidation flow.
const apply = (policy, d, k) =>
  policy === "single" ? d.remove(k) : policy === "all" ? d.removeAll("a-4471") : 0;
const living = (policy) => {
  const d = openLedger();
  const identifiers = [d.open("a-4471", "logged-in"), d.open("a-4471", "logged-in"), d.open("a-4471", "logged-in")];
  apply(policy, d, identifiers[0]);
  return d.live("a-4471");
};
const invalidations = FLOW.filter((a) => a.type === "invalidate");
const B = [-22, 12, 10, 12, 10];
console.log("\n2. invalidation flows (start: 3 live sessions per flow)");
write(B, "flow", "now", "living", "expected", "living");
let nowTotal = 0, patternTotal = 0;
for (const a of invalidations) {
  const s = living(a.now), b = living(expected(a));
  nowTotal += s; patternTotal += b;
  write(B, a.name, a.now, s, expected(a), b);
}
write(B, "total", "", nowTotal, "", patternTotal);

// 3. coverage: how many flows already do what the pattern expects
const covered = (set) => set.filter((a) => a.now === expected(a)).length;
const NEW_FLOW = { name: "field-device-swap", type: "upgrade", now: "in-place" };
const extended = [...FLOW, NEW_FLOW];
const C = [-34, 6, 10, 10, 12];
console.log("\n3. flow set coverage");
write(C, "set", "flows", "covered", "uncovered", "uncovered %");
for (const [name, k, central] of [
  ["today's distributed state", FLOW, false],
  ["central session pattern", FLOW, true],
  ["new flow added, distributed", extended, false],
  ["new flow added, central pattern", extended, true],
]) {
  const cov = central ? k.length : covered(k);
  write(C, name, k.length, cov, k.length - cov, (((k.length - cov) / k.length) * 100).toFixed(1));
}

// 4. the flow the pattern cannot reach: `subscription-transfer` changes the subscription
// record from outside the portal and may not call the session pattern. A version-bound
// pattern needs no call.
const transfer = (versioned, notifies) => {
  const d = openLedger({ versioned });
  d.open("a-4471", "logged-in"); d.open("a-4471", "logged-in"); d.open("a-4471", "logged-in");
  if (notifies) d.removeAll("a-4471");
  d.bumpVersion("a-4471");                      // the subscription record changes in every case
  return d.live("a-4471");
};
const D = [-24, 16, 16, 16];
console.log("\n4. subscription-transfer flow (3 live sessions): what happens if the pattern is not called");
write(D, "setup", "notified event", "living session", "covered flow");
for (const [name, versioned, notifies] of [
  ["call-bound pattern", false, true],
  ["call-bound pattern", false, false],
  ["version-bound pattern", true, false],
]) write(D, name, notifies ? "yes" : "no", transfer(versioned, notifies),
  `${versioned || notifies ? invalidations.length : invalidations.length - 1} / ${invalidations.length}`);
1. 4 upgrade flows: what the old identifier resolves to after login
flow                           now  old identifier's level      fixation point
portal-login                 renew            not-resolved                  no
second-factor-approval    in-place               logged-in                 YES
role-elevation            in-place               logged-in                 YES
field-app-pairing            renew            not-resolved                  no
session-identifier-generating flow: 4, not renewing: 2

2. invalidation flows (start: 3 live sessions per flow)
flow                           now    living    expected    living
logout                      single         2      single         2
password-change               none         3         all         0
subscription-transfer         none         3         all         0
total                                      8                     2

3. flow set coverage
set                                flows   covered uncovered uncovered %
today's distributed state              7         3         4        57.1
central session pattern                7         7         0         0.0
new flow added, distributed            8         3         5        62.5
new flow added, central pattern        8         8         0         0.0

4. subscription-transfer flow (3 live sessions): what happens if the pattern is not called
setup                     notified event  living session    covered flow
call-bound pattern                   yes               0           3 / 3
call-bound pattern                    no               3           2 / 3
version-bound pattern                 no               0           3 / 3

The numbers belong to the measurement class; their inputs are IE5–IE8.

Four Flows Generate, Two Do not Renew

The first table counts the upgrade side. Four flows change the session’s authorization level; two renew the identifier, two upgrade the record in place. In the two flows that upgrade in place, the old identifier continues to resolve to the logged-in level; in the two that renew, it is not-resolved.

Which two are the fixation points is instructive. portal-login is written correctly — this is the flow everyone looks at, and the rule is best known there. The two written wrong are second-factor-approval and role-elevation: both are flows that come after login and are not mistaken for login itself. When the rule is memorized as “the identifier is renewed at login,” flows that raise the level after login fall outside it.

This is also where the number’s meaning lies. The sentence “we are protected against session fixation” is true about one flow and half-true about four flows. Its measurable form is this: session-identifier-generating flow: 4, not renewing: 2.

It is also easy to see why in-place upgrading was written this way. Updating the record in place does less work: no new identifier is generated, no new value is sent to the client, requests in flight are not affected. Renewing, on the other hand, requires copying the record, deleting the old one, and having the client receive the new value. Both fixation points having chosen this cheap path is not carelessness — it is the rule never having been asked at that flow at all.

Eight Sessions Continue to Live On

The second table counts the invalidation side. Every flow runs on a subscriber who has three live sessions; what is measured is the number of records left in the ledger after the flow.

The logout flow deletes its own session, and two sessions remain. This is correct: logging out from one endpoint does not close sessions on the other endpoints. password-change and subscription-transfer, however, delete no record in the ledger at all; all three continue to live on. A total of eight sessions are still alive after the three flows where authorization is supposed to have been taken back.

Why the two flows are in this state has to do with where the work is done. The password-change flow’s visible job is updating the credential; touching the session ledger does not look like part of that job. Subscription transfer, in turn, is a flow that belongs to the party that manages the subscription, not the portal, and it may not even know the session ledger exists. In both cases, what is missing is not a check — it is a call.

How the number is measured also matters. Every flow runs on a subscriber who has three live sessions; the three sessions are the same person’s separate endpoints. As the number of sessions per subscriber grows, the number of sessions a non-invalidating flow leaves behind grows directly with it. This is why the unit of the measure is not “flow” but session living on after the flow: two missing flows are an item that grows as it is multiplied by the session count.

The expected column runs the same flows with the correct policy: single for logout, all for the other two. The total drops from eight to two, and the two remaining sessions are the sessions at the logout flow’s other endpoints — sessions that are not supposed to be closed. The measurement shows one more thing here: the correct number is not zero. If “close all” were correct for every flow, the user would also be dropped from the other endpoints when logging out from one.

The Flow the Pattern Covers

The third table counts coverage. Three of the seven flows do what the pattern expects; four are left uncovered — the uncovered rate is 57.1%.

The central session pattern is a single function and keeps two things in one place: which event corresponds to which policy, and how that policy is applied to the ledger. Flows do not make the decision themselves; they report the event. In this setup the number of covered flows is 7 and the uncovered count is zero — but this zero does not come from the pattern being clever, it comes from the decision having been made once.

The last two rows answer the new-flow question. The eighth flow — field-device-swap, a field technician replacing their terminal — raises the distributed setup’s uncovered count from 4 to 5 and the rate climbs to 62.5%. In the central pattern, the same flow reports an event and the uncovered count stays at zero. In a distributed setup, every new flow requires remembering both rules all over again; in the pattern, only writing the event’s name correctly.

The Flow That Does Not Call the Pattern

The fourth table exposes the assumption the third table’s zero rests on. The central pattern is a call-bound pattern: it runs when a flow reports the event, and does not run when it does not. This assumption holds for six of the seven flows without issue, because six of them are the portal’s own code.

subscription-transfer is different. The party that changes the subscription record is not the portal; the transfer is a separate flow that updates the subscription record directly. The table separates two cases: when the flow reports the event, the three sessions drop to zero; when it does not, all three continue to live on and the covered-flow count drops from 3 to 2. The central pattern stays right where it is, written correctly, and working; the only thing it does not cover is the flow that does not call it.

The last row changes the pattern’s shape. If the session record carries the subscriber’s authorization version, resolution compares the version in the record against the subscriber’s current version on every request. When the transfer flow changes the subscription record, the version increases regardless; even though it never calls the session pattern, all three sessions become invalid — living session: 0, covered flow: 3/3. The difference is where the defense is attached: a call-bound pattern waits for the flow to remember; a data-bound pattern uses a change the flow was already making as its criterion. The latter’s coverage stays constant as the flow set grows.

Summary

  • Session decisions reduce to two rules: a flow that raises the authorization level renews the identifier, and a flow that takes authorization back deletes the record server-side. Both are decided separately per flow.
  • Four of seven flows generate a session identifier; two are fixation points because they do not renew the identifier, and the two written wrong are flows that come after login.
  • Two of three invalidation flows delete no record in the ledger at all; eight sessions continue to live on after all three, and the correct policy brings this number down to two.
  • The correct number is not zero: the two sessions at the other endpoints living on after the logout flow is the expected outcome; the policy is set per flow.
  • Three of seven flows do what the pattern expects, four are left uncovered (57.1%); when an eighth flow is added, the uncovered count stays at zero in the central pattern while it rises to five in the distributed setup.
  • A call-bound pattern does not cover a flow that does not call it: when the transfer flow does not report the event, all three sessions live on. When the session record carries the subscriber’s authorization version, the same flow invalidates all three sessions without making any call at all.

Next Step

These two lessons secured the identity behind the request: who it is was established without leaking, and the carrier of that identity was renewed at the right moments and deleted at the right moments. Knowing the identity correctly, however, is only half the decision. When a known identity sends a request to an endpoint, there is a second question that has to be asked, and it is asked per endpoint: can this identity touch this object? The next lesson derives the authorization matrix and gives this topic’s most important number: how many endpoints perform only a role check, how many also check the ownership of the requested object, and whether the central pattern covers a newly added endpoint.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close