rules:
  - id: auth.flow.ssrf
    languages:
      - javascript
      - typescript
    severity: ERROR
    # Non-production code (example apps, demos, docs, vendored copies, tests)
    # is not the library surface users ship, so findings there are noise for a
    # low-FP linter. Globs intentionally omit `**/tests/**` / `**/fixtures/**`
    # so the rule still fires on its own fixtures under rules/tests/fixtures/.
    paths:
      exclude:
        - "**/test/**"
        - "**/__tests__/**"
        - "**/*.test.*"
        - "**/*.spec.*"
        - "**/example/**"
        - "**/examples/**"
        - "**/demo/**"
        - "**/docs/**"
        - "**/__mocks__/**"
        - "**/mocks/**"
        - "**/vendored/**"
        - "**/node_modules/**"
        - "**/*.stories.*"
    message: |
      Untrusted request input flows into the URL of an outbound HTTP request.
      Because the destination is attacker-controlled, this is a Server-Side
      Request Forgery (CWE-918): an attacker can point the request at internal
      services behind your firewall, or at the cloud metadata endpoint
      (http://169.254.169.254/...) to steal IAM/instance credentials and pivot
      deeper into your infrastructure.

      Never build a request URL straight from a raw `req.query` / `req.body` /
      `req.params` / `req.cookies` / `req.headers` value. Validate the
      destination against an explicit allow-list of hosts (resolve the URL and
      check its host against the allow-list, rejecting private/loopback ranges)
      before issuing the request.
    # Taint mode so indirection is caught: `const u = req.query.endpoint;
    # http.get(u)` flags, not just the direct `fetch(req.query.url)` form.
    # Passing the value through a host allow-list / validation call
    # (isAllowedUrl, allowlist.includes, Set.has) clears the taint, so a
    # genuinely vetted outbound request does not fire.
    mode: taint
    # `$REQ` is constrained to conventional request-object names so that
    # unrelated receivers (`db.query`, `config.headers`, `someObj.body`) are not
    # treated as untrusted request input.
    pattern-sources:
      - patterns:
          - pattern-either:
              - pattern: $REQ.query
              - pattern: $REQ.params
              - pattern: $REQ.body
              - pattern: $REQ.cookies
              - pattern: $REQ.headers
              - pattern: $REQ.query.$X
              - pattern: $REQ.params.$X
              - pattern: $REQ.body.$X
              - pattern: $REQ.cookies.$X
              - pattern: $REQ.headers.$X
              - pattern: $REQ.query[$K]
              - pattern: $REQ.params[$K]
              - pattern: $REQ.body[$K]
              - pattern: $REQ.cookies[$K]
              - pattern: $REQ.headers[$K]
          - metavariable-regex:
              metavariable: $REQ
              regex: ^(req|request|ctx|context|c|event|r)$
    pattern-sanitizers:
      # Routing the value through a host allow-list / validation helper clears
      # the taint: only the vetted destination (not the raw request input)
      # reaches the request.
      - pattern: isAllowedUrl(...)
      - pattern: validateUrl(...)
      - pattern: assertAllowedHost(...)
      # An inline allow-list membership guard vets the value: a value used inside
      # `if (allow.has(x)) { ... }` / `if (allow.includes(x)) { ... }` /
      # `if (allow.indexOf(x) ...) { ... }` is treated as validated. The
      # boolean-returning membership call itself does NOT sanitize its argument
      # (that would only clear the boolean, not the value), so we clear taint by
      # the if-guard, mirroring the Python rule's `if is_allowed_url(v): ...`.
      - patterns:
          - pattern: $X
          - pattern-inside: |
              if (<... $ALLOW.has($X) ...>) { ... }
      - patterns:
          - pattern: $X
          - pattern-inside: |
              if (<... $ALLOW.includes($X) ...>) { ... }
      - patterns:
          - pattern: $X
          - pattern-inside: |
              if (<... $ALLOW.indexOf($X) ...>) { ... }
    pattern-sinks:
      - patterns:
          - pattern-either:
              - pattern: fetch($SINK, ...)
              - pattern: nodeFetch($SINK, ...)
              - pattern: request($SINK, ...)
              - pattern: got($SINK, ...)
              - pattern: axios($SINK)
              - pattern: axios.get($SINK, ...)
              - pattern: axios.post($SINK, ...)
              - pattern: axios.put($SINK, ...)
              - pattern: axios.delete($SINK, ...)
              - pattern: "axios.request({..., url: $SINK, ...})"
              - pattern: http.get($SINK, ...)
              - pattern: https.get($SINK, ...)
              - pattern: http.request($SINK, ...)
              - pattern: https.request($SINK, ...)
          - focus-metavariable: $SINK
    metadata:
      oauthlint-rule-id: AUTH-FLOW-011
      oauthlint-doc-url: https://oauthlint.dev/rules/flow-ssrf
      category: security
      cwe: CWE-918
      owasp: API7:2023
      llm-prevalence: HIGH
      technology:
        - express
        - axios
        - fetch
      references:
        - https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
        - https://cwe.mitre.org/data/definitions/918.html
