rules:
  - id: auth.session.no-regeneration
    languages:
      - javascript
      - typescript
    severity: WARNING
    # Non-production code (example apps, demos, sample projects, benchmarks,
    # integration harnesses, docs, vendored copies, tests) is not the library
    # surface users ship, so findings there are noise for a low-FP linter.
    paths:
      exclude:
        - "**/test/**"
        - "**/__tests__/**"
        - "**/*.test.*"
        - "**/*.spec.*"
        - "**/example/**"
        - "**/examples/**"
        - "**/demo/**"
        - "**/sample/**"
        - "**/samples/**"
        - "**/benchmark/**"
        - "**/benchmarks/**"
        - "**/bench/**"
        - "**/integration/**"
        - "**/docs/**"
        - "**/__mocks__/**"
        - "**/mocks/**"
        - "**/vendored/**"
        - "**/node_modules/**"
        - "**/*.stories.*"
    message: |
      The user is marked as logged in without first regenerating the session id.
      The identity is written onto the session (`req.session.user = ...`,
      `req.session.userId = ...`, etc.) before the session id is rotated. This
      opens the door to session-fixation attacks: an attacker who plants a
      known session id in the victim's browser before login retains access
      after authentication succeeds.

      Call `req.session.regenerate(cb)` (or your framework's equivalent)
      between authenticating the credentials and writing the user
      identity onto the session.

      OWASP ASVS V3.2.1: "The session id must be regenerated on
      authentication".
    patterns:
      - pattern-either:
          - pattern: '$REQ.session.user = $X'
          - pattern: '$REQ.session.userId = $X'
          - pattern: '$REQ.session.user_id = $X'
          - pattern: '$REQ.session.uid = $X'
          - pattern: '$REQ.session.authenticated = true'
          - pattern: '$REQ.session.isAuthenticated = true'
      # Callback form: assignment inside regenerate's callback.
      - pattern-not-inside: |
          $REQ.session.regenerate(...)
      # Promisified async form: `await new Promise(r => req.session.regenerate(...))`
      # immediately before the assignment. Scoped to regenerate so it does NOT
      # suppress assignments that merely follow some unrelated await.
      - pattern-not-inside: |
          await new Promise(($R) => $REQ.session.regenerate(...));
          ...
      - pattern-not-inside: |
          await new Promise(($R, $J) => $REQ.session.regenerate(...));
          ...
      - pattern-not-inside: |
          await promisify($REQ.session.regenerate)(...);
          ...
    metadata:
      oauthlint-rule-id: AUTH-SESSION-002
      oauthlint-doc-url: https://oauthlint.dev/rules/session-no-regeneration
      category: security
      cwe: CWE-384
      owasp: API2:2023
      llm-prevalence: MEDIUM
      technology:
        - express
      references:
        - https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#renew-the-session-id-after-any-privilege-level-change
