diff --git a/crates/sessionscope-classifier/src/session_fixation.rs b/crates/sessionscope-classifier/src/session_fixation.rs index 6100095..02db434 100644 --- a/crates/sessionscope-classifier/src/session_fixation.rs +++ b/crates/sessionscope-classifier/src/session_fixation.rs @@ -271,7 +271,7 @@ fn framework_for<'a>(artifact: &'a Artifact, stores: &[&FixationRecord<'a>]) -> .flat_map(|record| record.artifact.framework_hints.iter()), ) .map(String::as_str) - .find(|hint| matches!(*hint, "express" | "cookie-session" | "django")) + .find(|hint| matches!(*hint, "express" | "cookie-session" | "django" | "nextjs")) .unwrap_or_else(|| { artifact .framework_hints @@ -292,6 +292,9 @@ fn suggested_fix_for_framework(framework: &str) -> &'static str { "django" => { "Use Django login(request, user), auth_login(request, user), or request.session.cycle_key() in the transition path so session rotation is visible." } + "nextjs" => { + "In a Next.js App Router route handler, call cookies().delete('session') followed immediately by cookies().set('session', newValue, options) at the authentication or privilege transition so cookie rotation is source-visible." + } _ => { "Identify the framework's session rotation primitive and call it during authentication and privilege transitions." } diff --git a/crates/sessionscope-detectors/src/sessions/mod.rs b/crates/sessionscope-detectors/src/sessions/mod.rs index 8c3ac10..374b1ef 100644 --- a/crates/sessionscope-detectors/src/sessions/mod.rs +++ b/crates/sessionscope-detectors/src/sessions/mod.rs @@ -704,7 +704,7 @@ fn collect_js_call_signal(node: Node<'_>, source: &str, signals: &mut Vec, source: &str, signals: &mut Vec, source: &str, signals: &mut Vec &'static str { } } +fn js_session_reissue_framework(text: &str) -> &'static str { + if text.contains("cookies()") + || text.contains(".cookies.delete") + || text.contains(".cookies.set") + { + "nextjs" + } else { + "cookie-session" + } +} + fn is_js_provider_session_config_call(normalized: &str) -> bool { contains_provider_context(normalized) && (normalized.contains("nextauth") diff --git a/crates/sessionscope-testing/src/fixtures.rs b/crates/sessionscope-testing/src/fixtures.rs index 521ca4e..3564ef7 100644 --- a/crates/sessionscope-testing/src/fixtures.rs +++ b/crates/sessionscope-testing/src/fixtures.rs @@ -71,7 +71,7 @@ pub fn fixture_cases() -> io::Result> { } pub fn load_expected_fixture(path: &Path) -> io::Result { - let contents = fs::read_to_string(path)?; + let contents = fs::read_to_string(path)?; // nosemgrep: rust.actix.path-traversal.tainted-path.tainted-path serde_json::from_str(&contents) .map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error.to_string())) } @@ -81,7 +81,7 @@ pub fn fixture_source_text(case: &FixtureCase) -> io::Result { + res.clearCookie("session"); + res.redirect("/"); +}); + +// Fixed: destroy the server-side session record before clearing the cookie +app.post("/logout", (req, res) => { + req.session.destroy(() => { + res.clearCookie("session"); + res.redirect("/"); + }); +}); +``` + +**Refresh token without rotation** + +```ts +// Vulnerable: new access token issued but old refresh token not revoked +app.post("/refresh", async (req, res) => { + const { refresh_token } = req.body; + await validateRefreshToken(refresh_token); + const newAccess = issueAccessToken(userId); + res.json({ access_token: newAccess }); +}); + +// Fixed: rotate the refresh token and revoke the previous one +app.post("/refresh", async (req, res) => { + const { refresh_token } = req.body; + const record = await validateRefreshToken(refresh_token); + await revokeRefreshToken(refresh_token); // mark old token used + const newRefresh = await issueRefreshToken(record.userId); + const newAccess = issueAccessToken(record.userId); + res.json({ access_token: newAccess, refresh_token: newRefresh }); +}); +``` + +**Password change without global session revocation** + +```ts +// Vulnerable: password changed but existing sessions not invalidated +app.post("/change-password", async (req, res) => { + await updatePassword(userId, req.body.newPassword); + res.json({ ok: true }); +}); + +// Fixed: revoke all active sessions after password change +app.post("/change-password", async (req, res) => { + await updatePassword(userId, req.body.newPassword); + await revokeAllSessions(userId); // invalidate all existing sessions/refresh tokens + res.json({ ok: true }); +}); +``` + ## See also - [`docs/SCHEMA.md`](SCHEMA.md) — JSON inventory and finding schema, diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 37b1473..28747c9 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -37,7 +37,7 @@ and [`crates/sessionscope-model/src/baseline.rs`](../crates/sessionscope-model/s | Surface | Constant | Current | Governs | | ------- | -------- | ------- | ------- | -| CLI release | `sessionscope` crate version (`Cargo.toml`) | `0.1.0` | CLI flags, command grammar, output paths, exit codes, `sessionscope.toml` keys, GitHub Action inputs | +| CLI release | `sessionscope` crate version (`Cargo.toml`) | `0.2.0` | CLI flags, command grammar, output paths, exit codes, `sessionscope.toml` keys, GitHub Action inputs | | Scan report | `SCHEMA_VERSION` (`schema.rs`) | `0.5.0` | `ScanReport` JSON inventory and findings shape; SARIF and Markdown render this same model | | Baseline | `BASELINE_SCHEMA_VERSION` (`baseline.rs`) | `0.1.0` | Baseline JSON wire format (`sessionscope baseline create` output) | | Diff | `DIFF_SCHEMA_VERSION` (`baseline.rs`) | `0.1.0` | Diff JSON wire format (`sessionscope diff` output) | diff --git a/fixtures/README.md b/fixtures/README.md index 6a40610..bc28f2f 100644 --- a/fixtures/README.md +++ b/fixtures/README.md @@ -36,13 +36,18 @@ Placeholder values intentionally look fake: | `express/` | `passport-oauth-strategy` | Passport OAuth strategy configuration, callback/session handling, provider refresh, and provider revocation evidence. | | `express/` | `refresh-rotation` | Refresh-token handler with lookup, old-token invalidation, new-token storage, and expiry evidence. | | `express/` | `refresh-without-rotation` | Refresh-token handler/use evidence without linked rotation or revocation evidence. | +| `express/` | `jwt-validation` | Express route handlers issuing/verifying `jsonwebtoken` JWTs, with a legacy verify that disables expiry enforcement and pins neither issuer nor audience, plus a decode-without-verify inspect route. | | `nextjs/` | `route-handler-auth` | Next.js-style route handlers for cookies, JWT validation, refresh, and logout. | | `nextjs/` | `authjs-nextauth-provider` | Auth.js/NextAuth provider configuration, JWT/session callbacks, provider-managed refresh, and logout revocation evidence. | | `nextjs/` | `nextresponse-session` | Next.js `NextResponse` cookie storage/deletion, route-local JWT validation, refresh rotation, and logout revocation evidence. | +| `nextjs/` | `session-fixation-signals` | Next.js App Router login and privilege-transition session-fixation signals, clear-and-reissue suppression, and logout-only suppression. | | `fastapi/` | `dependency-auth-lifecycle` | FastAPI dependency patterns for cookies, JWT claims, logout, and reset-token expiry. | | `fastapi/` | `cookie-posture-expanded` | Expanded FastAPI cookie posture checks and Set-Cookie header parsing. | | `fastapi/` | `security-dependencies` | FastAPI `Depends`, `Security`, `OAuth2PasswordBearer`, `APIKeyCookie`, response cookies, JWT validation, refresh revocation, and logout deletion. | +| `fastapi/` | `oauth-flow` | FastAPI router handlers running an Authlib `OAuth2Session` authorization-code flow with static state and a callback that reads state without visible verification. | +| `fastapi/` | `trust-boundary` | FastAPI-framed token reuse across inbound/outbound, frontend/backend, and cross-environment boundaries plus provider-managed token review. | | `django/` | `session-and-reset-flow` | Django settings/views for secure cookies, session logout, signing, and reset-token expiry. | +| `django/` | `trust-boundary` | Django-framed token reuse across inbound/outbound, frontend/backend, and cross-environment boundaries plus provider-managed token review. | | `django/` | `password-change-refresh-revoke` | Password-change-triggered refresh-token revocation evidence. | | `django/` | `settings-session-auth` | Django session cookie settings, login/session cycling, signing utilities, JWT helpers, refresh revocation, and logout/session flush evidence. | | `generic-ts/` | `jwt-validation` | Generic TypeScript JWT issue/verify cases for issuer, audience, expiry, and missing validation evidence. | @@ -50,7 +55,9 @@ Placeholder values intentionally look fake: | `generic-ts/` | `provider-refresh` | Provider-managed refresh behavior represented as dynamic review context. | | `generic-ts/` | `provider-revoke` | Provider abstraction revocation evidence without live provider calls. | | `generic-ts/` | `oidc-client-config` | OAuth/OIDC issuer, audience, scope, callback, refresh, and revocation configuration evidence. | -| `generic-ts/` | `cloud-identity-sdk` | Common cloud identity SDK token, refresh, scope, provider, and revoke/sign-out evidence. | +| `generic-ts/` | `cloud-identity-sdk` | Common cloud identity SDK token, refresh, scope, provider, and revoke/sign-out evidence (all seven SDKs together). | +| `generic-ts/` | `sdk-auth0` | Auth0 SDK client-credentials issue, refresh, and logout evidence (per-SDK breakdown of the combined cloud-identity fixture). | +| `generic-ts/` | `sdk-supabase` | Supabase Auth SDK session read, refresh, and sign-out evidence (per-SDK breakdown of the combined cloud-identity fixture). | | `generic-ts/` | `bearer-api-key-lifecycle` | Generic TypeScript opaque bearer token, service token, and API-key lifecycle evidence. | | `generic-ts/` | `trust-boundary-token-reuse` | TypeScript token scope, environment, frontend/backend, and trust-boundary reuse evidence. | | `generic-python/` | `jwt-and-reset` | Generic Python/PyJWT-style issue/verify cases and reset-token lifecycle examples. | diff --git a/fixtures/django/trust-boundary/config.yaml b/fixtures/django/trust-boundary/config.yaml new file mode 100644 index 0000000..3aafb2d --- /dev/null +++ b/fixtures/django/trust-boundary/config.yaml @@ -0,0 +1,2 @@ +production_service_token_env: PROD_SERVICE_TOKEN +staging_service_token_env: STAGING_SERVICE_TOKEN diff --git a/fixtures/django/trust-boundary/expected.json b/fixtures/django/trust-boundary/expected.json new file mode 100644 index 0000000..006ede1 --- /dev/null +++ b/fixtures/django/trust-boundary/expected.json @@ -0,0 +1,18 @@ +{ + "fixture_id": "django.trust-boundary", + "framework": "django", + "source_files": ["views.py", "src/client/app.py", "src/server/backend.py", "config.yaml"], + "expected_artifacts": [ + "opaque_bearer_token:authorization_bearer", + "api_key:api_key", + "service_token:service_token" + ], + "expected_lifecycle_stages": ["transmit", "introspect"], + "expected_findings": [ + "trust_boundary_inbound_outbound_reuse_review", + "trust_boundary_frontend_backend_reuse_review", + "trust_boundary_environment_reuse_review", + "trust_boundary_provider_scope_review" + ], + "notes": "Django-framed mirror of generic-python trust-boundary-token-reuse: same-token forwarding in Django view classes, frontend/backend API-key naming, cross-environment service-token config, and provider-wrapper review." +} diff --git a/fixtures/django/trust-boundary/src/client/app.py b/fixtures/django/trust-boundary/src/client/app.py new file mode 100644 index 0000000..3726d4e --- /dev/null +++ b/fixtures/django/trust-boundary/src/client/app.py @@ -0,0 +1,6 @@ +import os + + +def client_config(): + api_key = os.environ.get("NEXT_PUBLIC_API_KEY") + return {"api_key": api_key} diff --git a/fixtures/django/trust-boundary/src/server/backend.py b/fixtures/django/trust-boundary/src/server/backend.py new file mode 100644 index 0000000..9f04962 --- /dev/null +++ b/fixtures/django/trust-boundary/src/server/backend.py @@ -0,0 +1,11 @@ +import os + +import httpx + + +def server_api_key_call(): + api_key = os.environ["API_KEY"] + return httpx.get( + "https://billing.example.invalid/api", + headers={"X-API-Key": api_key}, + ) diff --git a/fixtures/django/trust-boundary/views.py b/fixtures/django/trust-boundary/views.py new file mode 100644 index 0000000..de57d59 --- /dev/null +++ b/fixtures/django/trust-boundary/views.py @@ -0,0 +1,31 @@ +import os + +import httpx +from django.http import JsonResponse +from django.views import View + + +class OrdersProxyView(View): + def get(self, request): + authorization = request.headers.get("Authorization") + return httpx.get( + "https://orders.example.invalid/api/orders", + headers={"Authorization": authorization}, + ) + + +class OrdersServiceView(View): + def get(self, request): + service_token = os.environ["ORDERS_TOKEN"] + return httpx.get( + "https://orders.example.invalid/api/orders", + headers={ + "X-Service-Token": service_token, + "audience": "orders_api", + }, + ) + + +class ProviderTokenView(View): + def get(self, request): + return provider_client.token(token="PLACEHOLDER_RESET_TOKEN") diff --git a/fixtures/express/jwt-validation/app.ts b/fixtures/express/jwt-validation/app.ts new file mode 100644 index 0000000..8f860e5 --- /dev/null +++ b/fixtures/express/jwt-validation/app.ts @@ -0,0 +1,60 @@ +import express from "express"; +import jwt from "jsonwebtoken"; + +const app = express(); +// Signing key is read from the environment rather than a literal so the fixture +// focuses on JWT validation posture, not secret hygiene. +const JWT_SECRET = process.env.JWT_SIGNING_SECRET ?? ""; +const ISSUER = "https://placeholder.issuer.invalid"; +const AUDIENCE = "placeholder-service"; + +export function issueAccessJwt(userId: string): string { + const claims = { + sub: userId, + tenant_id: "placeholder-tenant", + roles: ["admin"], + scope: "read:sessions", + }; + return jwt.sign(claims, JWT_SECRET, { + issuer: ISSUER, + audience: AUDIENCE, + expiresIn: "15m", + }); +} + +app.post("/sign", (req, res) => { + const token = issueAccessJwt(req.body.userId); + res.json({ token }); +}); + +// Verifies with issuer and audience pinned — the safe baseline path. +app.get("/protected", (req, res) => { + const header = req.headers.authorization; + const token = header && header.split(" ")[1]; + if (!token) return res.sendStatus(401); + const payload = jwt.verify(token, JWT_SECRET, { + issuer: ISSUER, + audience: AUDIENCE, + }); + res.json(payload); +}); + +// Legacy verify: disables expiry enforcement and pins neither issuer nor audience. +app.get("/legacy", (req, res) => { + const header = req.headers.authorization; + const token = header && header.split(" ")[1]; + if (!token) return res.sendStatus(401); + const payload = jwt.verify(token, JWT_SECRET, { ignoreExpiration: true }); + res.json(payload); +}); + +// Decodes without verifying the signature. +app.get("/inspect", (req, res) => { + const header = req.headers.authorization; + const token = header && header.split(" ")[1]; + if (!token) return res.sendStatus(401); + res.json(jwt.decode(token)); +}); + +export const placeholderJwt = + "PLACEHOLDER_HEADER.PLACEHOLDER_PAYLOAD.PLACEHOLDER_SIGNATURE"; diff --git a/fixtures/express/jwt-validation/expected.json b/fixtures/express/jwt-validation/expected.json new file mode 100644 index 0000000..fb9e7fc --- /dev/null +++ b/fixtures/express/jwt-validation/expected.json @@ -0,0 +1,14 @@ +{ + "fixture_id": "express.jwt-validation", + "framework": "express", + "source_files": ["app.ts"], + "expected_artifacts": ["access_jwt", "authorization_bearer"], + "expected_lifecycle_stages": ["issue", "validate", "transmit"], + "expected_findings": [ + "jwt_decode_without_verify", + "jwt_expiry_enforcement_disabled", + "jwt_missing_issuer", + "jwt_missing_audience" + ], + "notes": "Express route handlers issuing and verifying jsonwebtoken JWTs, with a legacy verify that disables expiry enforcement and pins neither issuer nor audience, and a decode-without-verify inspect route." +} diff --git a/fixtures/fastapi/oauth-flow/app.py b/fixtures/fastapi/oauth-flow/app.py new file mode 100644 index 0000000..21516f1 --- /dev/null +++ b/fixtures/fastapi/oauth-flow/app.py @@ -0,0 +1,17 @@ +from authlib import OAuth2Session +from fastapi import FastAPI, Request + +app = FastAPI() +router = app.router + + +@router.get("/auth") +def authorize(): + client = OAuth2Session("client-id") + return client.create_authorization_url("https://issuer.example/authorize", response_type="code", state="STATIC_STATE_PLACEHOLDER", code_challenge="PLACEHOLDER_CODE_CHALLENGE") + + +@router.get("/callback") +def callback(request: Request): + # State is read back from the query string but never compared to a stored value. + return request.query_params.get("state") diff --git a/fixtures/fastapi/oauth-flow/expected.json b/fixtures/fastapi/oauth-flow/expected.json new file mode 100644 index 0000000..b3ce65f --- /dev/null +++ b/fixtures/fastapi/oauth-flow/expected.json @@ -0,0 +1,12 @@ +{ + "fixture_id": "fastapi.oauth-flow", + "framework": "fastapi", + "source_files": ["app.py"], + "expected_artifacts": ["oauth_auth_code_flow"], + "expected_lifecycle_stages": ["issue"], + "expected_findings": [ + "oauth_state_static_review", + "oauth_state_unverified_review" + ], + "notes": "FastAPI-framed Authlib OAuth2Session authorization-code flow with static state and callback state read without visible verification, mirroring generic-ts oauth-flow-state patterns in FastAPI router handlers." +} diff --git a/fixtures/fastapi/trust-boundary/app.py b/fixtures/fastapi/trust-boundary/app.py new file mode 100644 index 0000000..95804ce --- /dev/null +++ b/fixtures/fastapi/trust-boundary/app.py @@ -0,0 +1,34 @@ +import os + +import httpx +from fastapi import Depends, FastAPI, Request +from fastapi.security import OAuth2PasswordBearer + +app = FastAPI() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token") + + +@app.get("/api/orders") +def forward_to_orders(request: Request): + authorization = request.headers.get("authorization") + return httpx.get( + "https://orders.example.invalid/api/orders", + headers={"Authorization": authorization}, + ) + + +@app.get("/api/orders/service") +def call_orders_with_service_token(): + service_token = os.environ["ORDERS_TOKEN"] + return httpx.get( + "https://orders.example.invalid/api/orders", + headers={ + "X-Service-Token": service_token, + "audience": "orders_api", + }, + ) + + +@app.get("/api/provider") +def provider_managed_token(provider: str = Depends(oauth2_scheme)): + return provider_client.token(token="PLACEHOLDER_RESET_TOKEN") diff --git a/fixtures/fastapi/trust-boundary/config.yaml b/fixtures/fastapi/trust-boundary/config.yaml new file mode 100644 index 0000000..3aafb2d --- /dev/null +++ b/fixtures/fastapi/trust-boundary/config.yaml @@ -0,0 +1,2 @@ +production_service_token_env: PROD_SERVICE_TOKEN +staging_service_token_env: STAGING_SERVICE_TOKEN diff --git a/fixtures/fastapi/trust-boundary/expected.json b/fixtures/fastapi/trust-boundary/expected.json new file mode 100644 index 0000000..d5e049c --- /dev/null +++ b/fixtures/fastapi/trust-boundary/expected.json @@ -0,0 +1,18 @@ +{ + "fixture_id": "fastapi.trust-boundary", + "framework": "fastapi", + "source_files": ["app.py", "src/client/app.py", "src/server/backend.py", "config.yaml"], + "expected_artifacts": [ + "opaque_bearer_token:authorization_bearer", + "api_key:api_key", + "service_token:service_token" + ], + "expected_lifecycle_stages": ["transmit", "introspect"], + "expected_findings": [ + "trust_boundary_inbound_outbound_reuse_review", + "trust_boundary_frontend_backend_reuse_review", + "trust_boundary_environment_reuse_review", + "trust_boundary_provider_scope_review" + ], + "notes": "FastAPI-framed mirror of generic-python trust-boundary-token-reuse: same-token forwarding in router handlers, frontend/backend API-key naming, cross-environment service-token config, and provider-wrapper review via Depends." +} diff --git a/fixtures/fastapi/trust-boundary/src/client/app.py b/fixtures/fastapi/trust-boundary/src/client/app.py new file mode 100644 index 0000000..3726d4e --- /dev/null +++ b/fixtures/fastapi/trust-boundary/src/client/app.py @@ -0,0 +1,6 @@ +import os + + +def client_config(): + api_key = os.environ.get("NEXT_PUBLIC_API_KEY") + return {"api_key": api_key} diff --git a/fixtures/fastapi/trust-boundary/src/server/backend.py b/fixtures/fastapi/trust-boundary/src/server/backend.py new file mode 100644 index 0000000..9f04962 --- /dev/null +++ b/fixtures/fastapi/trust-boundary/src/server/backend.py @@ -0,0 +1,11 @@ +import os + +import httpx + + +def server_api_key_call(): + api_key = os.environ["API_KEY"] + return httpx.get( + "https://billing.example.invalid/api", + headers={"X-API-Key": api_key}, + ) diff --git a/fixtures/generic-ts/cloud-identity-sdk/expected.json b/fixtures/generic-ts/cloud-identity-sdk/expected.json index 2185388..cb87707 100644 --- a/fixtures/generic-ts/cloud-identity-sdk/expected.json +++ b/fixtures/generic-ts/cloud-identity-sdk/expected.json @@ -5,5 +5,5 @@ "expected_artifacts": ["auth0:service_token", "okta:refresh_token", "cognito:refresh_token", "azure-ad:service_token", "firebase:unknown_token"], "expected_lifecycle_stages": ["refresh", "revoke", "transmit", "introspect"], "expected_findings": ["provider_boundary_review"], - "notes": "Covers common cloud identity SDK token, refresh, scope, provider, and revoke/sign-out evidence as dynamic static-analysis context." + "notes": "Covers common cloud identity SDK token, refresh, scope, provider, and revoke/sign-out evidence as dynamic static-analysis context. See sdk-auth0 and sdk-supabase for per-SDK breakdowns." } diff --git a/fixtures/generic-ts/sdk-auth0/app.ts b/fixtures/generic-ts/sdk-auth0/app.ts new file mode 100644 index 0000000..0916d92 --- /dev/null +++ b/fixtures/generic-ts/sdk-auth0/app.ts @@ -0,0 +1,18 @@ +// Auth0 SDK token lifecycle: client-credentials issue, refresh, and logout. +export async function auth0ClientCredentials() { + return auth0.clientCredentialsToken({ + audience: "orders-api", + scope: "orders:read", + }); +} + +export async function auth0Refresh() { + return auth0.oauth.refreshToken({ + refresh_token: "PLACEHOLDER_RESET_TOKEN", + scope: "offline_access", + }); +} + +export async function auth0Logout() { + return auth0.logout({ returnTo: "https://app.example.com/" }); +} diff --git a/fixtures/generic-ts/sdk-auth0/expected.json b/fixtures/generic-ts/sdk-auth0/expected.json new file mode 100644 index 0000000..59a5b31 --- /dev/null +++ b/fixtures/generic-ts/sdk-auth0/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "generic-ts.sdk-auth0", + "framework": "generic-ts", + "source_files": ["app.ts"], + "expected_artifacts": ["auth0:service_token", "refresh_token"], + "expected_lifecycle_stages": ["refresh", "revoke", "transmit", "introspect"], + "expected_findings": ["bearer_dynamic_provider_review"], + "notes": "Auth0 SDK client-credentials issue, refresh, and logout calls producing provider, scope, audience, refresh, and revoke evidence as dynamic static-analysis context." +} diff --git a/fixtures/generic-ts/sdk-supabase/app.ts b/fixtures/generic-ts/sdk-supabase/app.ts new file mode 100644 index 0000000..0ecc565 --- /dev/null +++ b/fixtures/generic-ts/sdk-supabase/app.ts @@ -0,0 +1,15 @@ +// Supabase Auth SDK token lifecycle: session read, refresh, and sign-out. +export async function supabaseSession() { + const { data } = await supabase.auth.getSession(); + return data.session?.access_token; +} + +export async function supabaseRefresh() { + return supabase.auth.refreshSession({ + refresh_token: "PLACEHOLDER_RESET_TOKEN", + }); +} + +export async function supabaseLogout() { + await supabase.auth.signOut(); +} diff --git a/fixtures/generic-ts/sdk-supabase/expected.json b/fixtures/generic-ts/sdk-supabase/expected.json new file mode 100644 index 0000000..bc13613 --- /dev/null +++ b/fixtures/generic-ts/sdk-supabase/expected.json @@ -0,0 +1,9 @@ +{ + "fixture_id": "generic-ts.sdk-supabase", + "framework": "generic-ts", + "source_files": ["app.ts"], + "expected_artifacts": ["session", "refresh_token"], + "expected_lifecycle_stages": ["refresh", "revoke", "introspect"], + "expected_findings": ["bearer_dynamic_provider_review"], + "notes": "Supabase Auth SDK session read, refresh, and sign-out calls producing provider, refresh, and revoke evidence as dynamic static-analysis context." +} diff --git a/fixtures/nextjs/session-fixation-signals/expected.json b/fixtures/nextjs/session-fixation-signals/expected.json new file mode 100644 index 0000000..4c3396b --- /dev/null +++ b/fixtures/nextjs/session-fixation-signals/expected.json @@ -0,0 +1,12 @@ +{ + "fixture_id": "nextjs.session-fixation-signals", + "framework": "nextjs", + "source_files": ["route.ts"], + "expected_artifacts": ["session_record:session"], + "expected_lifecycle_stages": ["issue", "store", "refresh", "revoke"], + "expected_findings": [ + "session_fixation_login_regeneration_review", + "session_fixation_privilege_regeneration_review" + ], + "notes": "Covers Next.js App Router login and privilege-transition session fixation signals, explicit clear-and-reissue rotation (suppressed), and logout-only suppression. Verifies that suggested_fix mentions cookies().set clear-and-reissue pattern." +} diff --git a/fixtures/nextjs/session-fixation-signals/route.ts b/fixtures/nextjs/session-fixation-signals/route.ts new file mode 100644 index 0000000..4141a65 --- /dev/null +++ b/fixtures/nextjs/session-fixation-signals/route.ts @@ -0,0 +1,54 @@ +import { cookies } from "next/headers"; +// Helpers are imported (not defined here) so the fixture exercises handler-level +// transition detection without helper definitions emitting their own signals. +import { authenticateUser, elevateUserRole, loadSession } from "@/lib/auth"; + +// Login handler that writes the session cookie without any clear-and-reissue. +// No rotation evidence sits near the auth transition, so this handler should be +// flagged for login regeneration review. +export async function POST(request: Request) { + const body = await request.json(); + const session = await authenticateUser(body.email, body.credential); + cookies().set("session", session.token, { + httpOnly: true, + secure: true, + sameSite: "strict", + }); + return Response.json({ ok: true }); +} + +// Login handler that performs an explicit clear-and-reissue rotation: +// cookies().delete then cookies().set at the auth transition. This emits a +// nextjs-hinted reissue signal in the same handler scope and is suppressed. +export async function PUT(request: Request) { + const body = await request.json(); + const session = await authenticateUser(body.email, body.credential); + cookies().delete("session"); + cookies().set("session", session.token, { + httpOnly: true, + secure: true, + sameSite: "strict", + }); + return Response.json({ ok: true }); +} + +// Privilege-elevation handler that rewrites the session at a privilege change +// without rotation. This should be flagged for privilege regeneration review. +export async function PATCH(request: Request) { + const body = await request.json(); + const session = await loadSession(body.userId); + await elevateUserRole(body.userId); + cookies().set("session", session.token, { + httpOnly: true, + secure: true, + sameSite: "strict", + }); + return Response.json({ ok: true }); +} + +// Logout handler — deletes the session cookie. Logout context is excluded from +// session-fixation review, so no finding is expected here. +export async function DELETE() { + cookies().delete("session"); + return new Response(null, { status: 204 }); +} diff --git a/tests/integration/snapshots/nextjs-session-fixation-signals.json b/tests/integration/snapshots/nextjs-session-fixation-signals.json new file mode 100644 index 0000000..76665a7 --- /dev/null +++ b/tests/integration/snapshots/nextjs-session-fixation-signals.json @@ -0,0 +1,3058 @@ +{ + "schema_version": "0.5.0", + "summary": { + "files_discovered": 2, + "files_scanned": 2, + "files_skipped": 0, + "diagnostics": [], + "worker_panic_count": 0 + }, + "files": [ + { + "path": "expected.json", + "language": "json", + "artifacts": [], + "evidence": [], + "diagnostics": [], + "skipped_reason": null + }, + { + "path": "route.ts", + "language": "type_script", + "artifacts": [ + { + "id": "artifact_99519f9260c91e23", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_3d3721d5557db8b5", + "evidence_99519f9260c91e23", + "evidence_b948b98a00509421" + ], + "transmit": [ + "evidence_2b0d1e6a719a2619", + "evidence_a0e3668a642dfaaa", + "evidence_c06f287be525771c", + "evidence_c435c61b74e89aee" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_6a754100e74234eb", + "evidence_956045f2a4804cf1" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_3d3721d5557db8b5" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_a0e3668a642dfaaa" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_c435c61b74e89aee" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_6a754100e74234eb" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_956045f2a4804cf1" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_c06f287be525771c" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_2b0d1e6a719a2619" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_c55afc15dd33b9be", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_c73cfc15da03eadf" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:488" + ] + }, + { + "id": "artifact_deeebd0bbdd45ffb", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_50d5a58d2549194a" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:488" + ] + }, + { + "id": "artifact_742c22bfa98cea1d", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 24, + "column": 22 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_eeedd243f64fbc5a" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1020" + ] + }, + { + "id": "artifact_2fc056c2538c5e41", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 25, + "column": 25 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_9bf89b9141c33726" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1120" + ] + }, + { + "id": "artifact_e471ebd216b38479", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_3219ab6daadfb28a" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_95660b363942d33a", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_4b40242c8321cb5b" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_f4173808ce169e6b", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2b31d1ca0b052515", + "evidence_367fb3d54df7a903", + "evidence_f4173808ce169e6b" + ], + "transmit": [ + "evidence_873fc9693f11a36a", + "evidence_d19aa01813414a74", + "evidence_f5451ed3d1ae2351", + "evidence_fe3a9022e31acd6d" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_d7b66625fd205465", + "evidence_fcad604cfc3474d3" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_367fb3d54df7a903" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_873fc9693f11a36a" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_f5451ed3d1ae2351" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_fcad604cfc3474d3" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_d7b66625fd205465" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_d19aa01813414a74" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_fe3a9022e31acd6d" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_0d25360c4c47b333", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_98ddebfe60cf5792" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_45f17f344b71c94f", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_8bc4a4a08455b936" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_dd6303eced33d54e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_a72e2ec25568f78f" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_a085aebdb63371d5", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 32, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_a019730c1dc7a782" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1020" + ] + }, + { + "id": "artifact_52cfbfe36d007073", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 37, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_e7570a6d18974084" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_2df5e44f4432b20b", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 41, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_04f8c31e90e7e4b7", + "evidence_2df5e44f4432b20b", + "evidence_834bca42cdf96585" + ], + "transmit": [ + "evidence_272182b496d23630", + "evidence_6ddc0afb49416ef6", + "evidence_c70cd3fc49cae0e8", + "evidence_c8c23f316474b25d" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_4f4ccb8489aab015", + "evidence_960e50a9d83c8573" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_04f8c31e90e7e4b7" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_6ddc0afb49416ef6" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_c70cd3fc49cae0e8" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_960e50a9d83c8573" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_4f4ccb8489aab015" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_272182b496d23630" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_c8c23f316474b25d" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_3b49a98d26cb553e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 41, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_8f2bf6a4d38d2d7b" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1516" + ] + }, + { + "id": "artifact_33d4d698e91be478", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 51, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_d99bc1ffd2bfff68" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_0b2a3124133d03eb", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 51, + "column": 8 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_55fe412eaccd899b" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_3ab6030ae44cd434", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 52, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_59583febe6244fbb" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + } + ], + "evidence": [ + { + "id": "evidence_50d5a58d2549194a", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b948b98a00509421", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_99519f9260c91e23", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const session = await authenticateUser(body.email, body.credential);\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c73cfc15da03eadf", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b0d1e6a719a2619", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c06f287be525771c", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_956045f2a4804cf1", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6a754100e74234eb", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3d3721d5557db8b5", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 13, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a0e3668a642dfaaa", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 14, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c435c61b74e89aee", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 15, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_eeedd243f64fbc5a", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 24, + "column": 22 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "request.json()", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_9bf89b9141c33726", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 25, + "column": 25 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "authenticateUser(body.email, body.credential)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_4b40242c8321cb5b", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 26, + "column": 3 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "cookies().delete(\"session\")", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_3219ab6daadfb28a", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 26, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"session\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_98ddebfe60cf5792", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b31d1ca0b052515", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f4173808ce169e6b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " cookies().delete(\"session\");\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a72e2ec25568f78f", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fe3a9022e31acd6d", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d19aa01813414a74", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_8bc4a4a08455b936", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_d7b66625fd205465", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fcad604cfc3474d3", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_367fb3d54df7a903", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 28, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_873fc9693f11a36a", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 29, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f5451ed3d1ae2351", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 30, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a019730c1dc7a782", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 32, + "column": 10 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "Response.json({ ok: true })", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_e7570a6d18974084", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 37, + "column": 1 + }, + "detector_id": "session.privilege_transition", + "confidence": "medium", + "excerpt": "export async function PATCH(request: Request) {\n const body = await request.json();\n const session = await loadSession(body.userId);\n await elevateUserRole(body.userId);\n cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n return Response.json({ ok: true });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_834bca42cdf96585", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2df5e44f4432b20b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " await elevateUserRole(body.userId);\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f2bf6a4d38d2d7b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c8c23f316474b25d", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_272182b496d23630", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_4f4ccb8489aab015", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_960e50a9d83c8573", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_04f8c31e90e7e4b7", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 42, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6ddc0afb49416ef6", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 43, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c70cd3fc49cae0e8", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 44, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d99bc1ffd2bfff68", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 51, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "export async function DELETE() {\n cookies().delete(\"session\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_55fe412eaccd899b", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 51, + "column": 8 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "async function DELETE() {\n cookies().delete(\"session\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_59583febe6244fbb", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 52, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"session\")", + "dynamic": false, + "framework_default": false + } + ], + "diagnostics": [], + "skipped_reason": null + } + ], + "artifacts": [ + { + "id": "artifact_99519f9260c91e23", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_3d3721d5557db8b5", + "evidence_99519f9260c91e23", + "evidence_b948b98a00509421" + ], + "transmit": [ + "evidence_2b0d1e6a719a2619", + "evidence_a0e3668a642dfaaa", + "evidence_c06f287be525771c", + "evidence_c435c61b74e89aee" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_6a754100e74234eb", + "evidence_956045f2a4804cf1" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_3d3721d5557db8b5" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_a0e3668a642dfaaa" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_c435c61b74e89aee" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_6a754100e74234eb" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_956045f2a4804cf1" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_c06f287be525771c" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_2b0d1e6a719a2619" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_c55afc15dd33b9be", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_c73cfc15da03eadf" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:488" + ] + }, + { + "id": "artifact_deeebd0bbdd45ffb", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 12, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_50d5a58d2549194a" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:488" + ] + }, + { + "id": "artifact_742c22bfa98cea1d", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 24, + "column": 22 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_eeedd243f64fbc5a" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1020" + ] + }, + { + "id": "artifact_2fc056c2538c5e41", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 25, + "column": 25 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_9bf89b9141c33726" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1120" + ] + }, + { + "id": "artifact_e471ebd216b38479", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_3219ab6daadfb28a" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_95660b363942d33a", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 26, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_4b40242c8321cb5b" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_f4173808ce169e6b", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_2b31d1ca0b052515", + "evidence_367fb3d54df7a903", + "evidence_f4173808ce169e6b" + ], + "transmit": [ + "evidence_873fc9693f11a36a", + "evidence_d19aa01813414a74", + "evidence_f5451ed3d1ae2351", + "evidence_fe3a9022e31acd6d" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_d7b66625fd205465", + "evidence_fcad604cfc3474d3" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_367fb3d54df7a903" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_873fc9693f11a36a" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_f5451ed3d1ae2351" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_fcad604cfc3474d3" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_d7b66625fd205465" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_d19aa01813414a74" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_fe3a9022e31acd6d" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_0d25360c4c47b333", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_98ddebfe60cf5792" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_45f17f344b71c94f", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_8bc4a4a08455b936" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_dd6303eced33d54e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 27, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_a72e2ec25568f78f" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1020" + ] + }, + { + "id": "artifact_a085aebdb63371d5", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 32, + "column": 10 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [ + "evidence_a019730c1dc7a782" + ], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "cookie-session", + "scope:1020" + ] + }, + { + "id": "artifact_52cfbfe36d007073", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 37, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [ + "evidence_e7570a6d18974084" + ], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_2df5e44f4432b20b", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 41, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_04f8c31e90e7e4b7", + "evidence_2df5e44f4432b20b", + "evidence_834bca42cdf96585" + ], + "transmit": [ + "evidence_272182b496d23630", + "evidence_6ddc0afb49416ef6", + "evidence_c70cd3fc49cae0e8", + "evidence_c8c23f316474b25d" + ], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [ + "evidence_4f4ccb8489aab015", + "evidence_960e50a9d83c8573" + ], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ], + "cookie_attributes": { + "http_only": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_04f8c31e90e7e4b7" + ], + "confidence": "high" + }, + "secure": { + "state": "present", + "value": "true", + "evidence_ids": [ + "evidence_6ddc0afb49416ef6" + ], + "confidence": "high" + }, + "same_site": { + "state": "present", + "value": "strict", + "evidence_ids": [ + "evidence_c70cd3fc49cae0e8" + ], + "confidence": "high" + }, + "max_age": { + "state": "missing", + "evidence_ids": [ + "evidence_960e50a9d83c8573" + ], + "confidence": "high" + }, + "expires": { + "state": "missing", + "evidence_ids": [ + "evidence_4f4ccb8489aab015" + ], + "confidence": "high" + }, + "path": { + "state": "framework_default", + "value": "/", + "evidence_ids": [ + "evidence_272182b496d23630" + ], + "confidence": "low" + }, + "domain": { + "state": "missing", + "evidence_ids": [ + "evidence_c8c23f316474b25d" + ], + "confidence": "high" + } + } + }, + { + "id": "artifact_3b49a98d26cb553e", + "artifact_type": "session_record", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 41, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [ + "evidence_8f2bf6a4d38d2d7b" + ], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs", + "scope:1516" + ] + }, + { + "id": "artifact_33d4d698e91be478", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 51, + "column": 1 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_d99bc1ffd2bfff68" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "nextjs" + ] + }, + { + "id": "artifact_0b2a3124133d03eb", + "artifact_type": "unknown", + "display_name": "logout", + "locations": [ + { + "path": "route.ts", + "line": 51, + "column": 8 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_55fe412eaccd899b" + ], + "expire": [], + "introspect": [] + }, + "confidence": "medium", + "framework_hints": [ + "javascript" + ] + }, + { + "id": "artifact_3ab6030ae44cd434", + "artifact_type": "session_cookie", + "display_name": "session", + "locations": [ + { + "path": "route.ts", + "line": 52, + "column": 3 + } + ], + "lifecycle_evidence": { + "issue": [], + "store": [], + "transmit": [], + "validate": [], + "refresh": [], + "revoke": [ + "evidence_59583febe6244fbb" + ], + "expire": [], + "introspect": [] + }, + "confidence": "high", + "framework_hints": [ + "nextjs" + ] + } + ], + "evidence": [ + { + "id": "evidence_50d5a58d2549194a", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_b948b98a00509421", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_99519f9260c91e23", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " const session = await authenticateUser(body.email, body.credential);\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c73cfc15da03eadf", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b0d1e6a719a2619", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c06f287be525771c", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_956045f2a4804cf1", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6a754100e74234eb", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 12, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_3d3721d5557db8b5", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 13, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a0e3668a642dfaaa", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 14, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c435c61b74e89aee", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 15, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_eeedd243f64fbc5a", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 24, + "column": 22 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "request.json()", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_9bf89b9141c33726", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 25, + "column": 25 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "authenticateUser(body.email, body.credential)", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_4b40242c8321cb5b", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 26, + "column": 3 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "cookies().delete(\"session\")", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_3219ab6daadfb28a", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 26, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"session\")", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_98ddebfe60cf5792", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.auth_transition", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2b31d1ca0b052515", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f4173808ce169e6b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " cookies().delete(\"session\");\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a72e2ec25568f78f", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fe3a9022e31acd6d", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d19aa01813414a74", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_8bc4a4a08455b936", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_d7b66625fd205465", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_fcad604cfc3474d3", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 27, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_367fb3d54df7a903", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 28, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_873fc9693f11a36a", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 29, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_f5451ed3d1ae2351", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 30, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_a019730c1dc7a782", + "lifecycle_stage": "refresh", + "location": { + "path": "route.ts", + "line": 32, + "column": 10 + }, + "detector_id": "session.reissue", + "confidence": "medium", + "excerpt": "Response.json({ ok: true })", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_e7570a6d18974084", + "lifecycle_stage": "issue", + "location": { + "path": "route.ts", + "line": 37, + "column": 1 + }, + "detector_id": "session.privilege_transition", + "confidence": "medium", + "excerpt": "export async function PATCH(request: Request) {\n const body = await request.json();\n const session = await loadSession(body.userId);\n await elevateUserRole(body.userId);\n cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n });\n return Response.json({ ok: true });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_834bca42cdf96585", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.name_prefix", + "confidence": "high", + "excerpt": "Cookie name prefix: none", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_2df5e44f4432b20b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.set", + "confidence": "high", + "excerpt": " await elevateUserRole(body.userId);\n cookies().set(\"session\", [REDACTED], {\n httpOnly: true,", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_8f2bf6a4d38d2d7b", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "session.store_after_auth", + "confidence": "high", + "excerpt": "cookies().set(\"[REDACTED]\", session.token, {\n httpOnly: true,\n secure: true,\n sameSite: \"[REDACTED]\",\n })", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c8c23f316474b25d", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.domain", + "confidence": "high", + "excerpt": "Domain is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_272182b496d23630", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.path", + "confidence": "low", + "excerpt": "Path defaults to / for nextjs", + "dynamic": false, + "framework_default": true + }, + { + "id": "evidence_4f4ccb8489aab015", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.expires", + "confidence": "high", + "excerpt": "Expires is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_960e50a9d83c8573", + "lifecycle_stage": "expire", + "location": { + "path": "route.ts", + "line": 41, + "column": 3 + }, + "detector_id": "cookie.attribute.max_age", + "confidence": "high", + "excerpt": "Max-Age is omitted", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_04f8c31e90e7e4b7", + "lifecycle_stage": "store", + "location": { + "path": "route.ts", + "line": 42, + "column": 15 + }, + "detector_id": "cookie.attribute.http_only", + "confidence": "high", + "excerpt": "HttpOnly: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_6ddc0afb49416ef6", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 43, + "column": 13 + }, + "detector_id": "cookie.attribute.secure", + "confidence": "high", + "excerpt": "Secure: true", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_c70cd3fc49cae0e8", + "lifecycle_stage": "transmit", + "location": { + "path": "route.ts", + "line": 44, + "column": 15 + }, + "detector_id": "cookie.attribute.same_site", + "confidence": "high", + "excerpt": "SameSite: \"strict\"", + "dynamic": false, + "framework_default": false + }, + { + "id": "evidence_d99bc1ffd2bfff68", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 51, + "column": 1 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "export async function DELETE() {\n cookies().delete(\"session\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_55fe412eaccd899b", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 51, + "column": 8 + }, + "detector_id": "logout.handler", + "confidence": "medium", + "excerpt": "async function DELETE() {\n cookies().delete(\"session\");\n return new Response(null, { status: 204 });\n}", + "dynamic": true, + "framework_default": false + }, + { + "id": "evidence_59583febe6244fbb", + "lifecycle_stage": "revoke", + "location": { + "path": "route.ts", + "line": 52, + "column": 3 + }, + "detector_id": "logout.cookie_clear", + "confidence": "high", + "excerpt": "cookies().delete(\"session\")", + "dynamic": false, + "framework_default": false + } + ], + "lifecycle_paths": [ + { + "id": "lifecycle_path_b811f0d2e52aca82", + "artifact_ids": [ + "artifact_0b2a3124133d03eb", + "artifact_33d4d698e91be478" + ], + "stages": [ + { + "stage": "revoke", + "evidence_ids": [ + "evidence_55fe412eaccd899b", + "evidence_d99bc1ffd2bfff68" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `logout`?" + }, + { + "id": "lifecycle_path_ebe806744c0044e1", + "artifact_ids": [ + "artifact_0d25360c4c47b333" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_98ddebfe60cf5792" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_05e0aa81388ab153", + "artifact_ids": [ + "artifact_2df5e44f4432b20b" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_04f8c31e90e7e4b7", + "evidence_2df5e44f4432b20b", + "evidence_834bca42cdf96585" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_272182b496d23630", + "evidence_6ddc0afb49416ef6", + "evidence_c70cd3fc49cae0e8", + "evidence_c8c23f316474b25d" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_4f4ccb8489aab015", + "evidence_960e50a9d83c8573" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_fe12ea879b434ef8", + "artifact_ids": [ + "artifact_2fc056c2538c5e41" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_9bf89b9141c33726" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_291b0d3d0113fa62", + "artifact_ids": [ + "artifact_3ab6030ae44cd434", + "artifact_99519f9260c91e23", + "artifact_e471ebd216b38479" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_3d3721d5557db8b5", + "evidence_99519f9260c91e23", + "evidence_b948b98a00509421" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_2b0d1e6a719a2619", + "evidence_a0e3668a642dfaaa", + "evidence_c06f287be525771c", + "evidence_c435c61b74e89aee" + ] + }, + { + "stage": "revoke", + "evidence_ids": [ + "evidence_3219ab6daadfb28a", + "evidence_59583febe6244fbb" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_6a754100e74234eb", + "evidence_956045f2a4804cf1" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_6f9f19388b7bb17c", + "artifact_ids": [ + "artifact_3b49a98d26cb553e" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_8f2bf6a4d38d2d7b" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_d1d0a1497667bd49", + "artifact_ids": [ + "artifact_45f17f344b71c94f" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_8bc4a4a08455b936" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_b0d4bdcf5c072242", + "artifact_ids": [ + "artifact_52cfbfe36d007073" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_e7570a6d18974084" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_1e5e0d40deac4b31", + "artifact_ids": [ + "artifact_742c22bfa98cea1d" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_eeedd243f64fbc5a" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_3e95d05a1b31e4c0", + "artifact_ids": [ + "artifact_95660b363942d33a" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_4b40242c8321cb5b" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_0373fed38d87c6fc", + "artifact_ids": [ + "artifact_a085aebdb63371d5" + ], + "stages": [ + { + "stage": "refresh", + "evidence_ids": [ + "evidence_a019730c1dc7a782" + ] + } + ], + "confidence": "medium", + "dynamic": true, + "reviewer_question": "Which production code path determines the effective lifecycle behavior for `session`?" + }, + { + "id": "lifecycle_path_642d8e023605076c", + "artifact_ids": [ + "artifact_c55afc15dd33b9be" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_c73cfc15da03eadf" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_4b7630722290aff5", + "artifact_ids": [ + "artifact_dd6303eced33d54e" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_a72e2ec25568f78f" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_9d8893361691db69", + "artifact_ids": [ + "artifact_deeebd0bbdd45ffb" + ], + "stages": [ + { + "stage": "issue", + "evidence_ids": [ + "evidence_50d5a58d2549194a" + ] + } + ], + "confidence": "high", + "dynamic": false, + "reviewer_question": null + }, + { + "id": "lifecycle_path_435f03708762e5a3", + "artifact_ids": [ + "artifact_f4173808ce169e6b" + ], + "stages": [ + { + "stage": "store", + "evidence_ids": [ + "evidence_2b31d1ca0b052515", + "evidence_367fb3d54df7a903", + "evidence_f4173808ce169e6b" + ] + }, + { + "stage": "transmit", + "evidence_ids": [ + "evidence_873fc9693f11a36a", + "evidence_d19aa01813414a74", + "evidence_f5451ed3d1ae2351", + "evidence_fe3a9022e31acd6d" + ] + }, + { + "stage": "expire", + "evidence_ids": [ + "evidence_d7b66625fd205465", + "evidence_fcad604cfc3474d3" + ] + } + ], + "confidence": "low", + "dynamic": false, + "reviewer_question": "Which framework version and deployment settings determine lifecycle behavior for `session`?" + } + ], + "findings": [ + { + "id": "finding_e8eb4f9bb52541eb", + "category": "dynamic_review_required", + "severity": "medium", + "artifact_ids": [ + "artifact_3b49a98d26cb553e", + "artifact_52cfbfe36d007073", + "artifact_c55afc15dd33b9be", + "artifact_dd6303eced33d54e" + ], + "evidence_ids": [ + "evidence_8f2bf6a4d38d2d7b", + "evidence_a72e2ec25568f78f", + "evidence_c73cfc15da03eadf", + "evidence_e7570a6d18974084" + ], + "title": "Session regeneration evidence was not found near a privilege transition", + "description": "A privilege-changing session transition was detected, but nearby static evidence did not show an explicit session regeneration, cookie reissue, or recognized framework-default rotation point.", + "suggested_fix": "In a Next.js App Router route handler, call cookies().delete('session') followed immediately by cookies().set('session', newValue, options) at the authentication or privilege transition so cookie rotation is source-visible.", + "reviewer_question": "Where is the session identifier rotated after this privilege change?" + }, + { + "id": "finding_3d62f4288cf95f60", + "category": "dynamic_review_required", + "severity": "medium", + "artifact_ids": [ + "artifact_3b49a98d26cb553e", + "artifact_c55afc15dd33b9be", + "artifact_dd6303eced33d54e", + "artifact_deeebd0bbdd45ffb" + ], + "evidence_ids": [ + "evidence_50d5a58d2549194a", + "evidence_8f2bf6a4d38d2d7b", + "evidence_a72e2ec25568f78f", + "evidence_c73cfc15da03eadf" + ], + "title": "Session regeneration evidence was not found near login", + "description": "An authentication transition was detected, but nearby static evidence did not show an explicit session regeneration, cookie reissue, or recognized framework-default rotation point.", + "suggested_fix": "In a Next.js App Router route handler, call cookies().delete('session') followed immediately by cookies().set('session', newValue, options) at the authentication or privilege transition so cookie rotation is source-visible.", + "reviewer_question": "Where is the session identifier rotated after this authentication transition?" + }, + { + "id": "finding_46d68eb0fe140175", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_2df5e44f4432b20b" + ], + "evidence_ids": [ + "evidence_4f4ccb8489aab015", + "evidence_960e50a9d83c8573" + ], + "title": "Cookie `session` has no explicit expiry evidence", + "description": "No Max-Age or Expires evidence was detected for this cookie-setting call.", + "suggested_fix": "Add an explicit Max-Age or Expires value when the cookie should have a bounded lifetime.", + "reviewer_question": "Should this cookie be session-scoped, or should it have an explicit lifetime?" + }, + { + "id": "finding_fc6116f0da3e1ea7", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_99519f9260c91e23" + ], + "evidence_ids": [ + "evidence_3219ab6daadfb28a", + "evidence_59583febe6244fbb" + ], + "title": "Cookie `session` is cleared on logout without linked server-side revocation", + "description": "Logout evidence clears a client-side cookie, but no linked server-side session, token, or provider revocation evidence was found for the same lifecycle path.", + "suggested_fix": "Invalidate the server-side session or refresh token in addition to deleting the browser cookie.", + "reviewer_question": "Where is the server-side session or token behind `session` revoked during logout?" + }, + { + "id": "finding_854daf14c6317c20", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_99519f9260c91e23" + ], + "evidence_ids": [ + "evidence_6a754100e74234eb", + "evidence_956045f2a4804cf1" + ], + "title": "Cookie `session` has no explicit expiry evidence", + "description": "No Max-Age or Expires evidence was detected for this cookie-setting call.", + "suggested_fix": "Add an explicit Max-Age or Expires value when the cookie should have a bounded lifetime.", + "reviewer_question": "Should this cookie be session-scoped, or should it have an explicit lifetime?" + }, + { + "id": "finding_2bf9178797e38511", + "category": "lifecycle_gap", + "severity": "low", + "artifact_ids": [ + "artifact_f4173808ce169e6b" + ], + "evidence_ids": [ + "evidence_d7b66625fd205465", + "evidence_fcad604cfc3474d3" + ], + "title": "Cookie `session` has no explicit expiry evidence", + "description": "No Max-Age or Expires evidence was detected for this cookie-setting call.", + "suggested_fix": "Add an explicit Max-Age or Expires value when the cookie should have a bounded lifetime.", + "reviewer_question": "Should this cookie be session-scoped, or should it have an explicit lifetime?" + } + ] +} \ No newline at end of file