OpenAI Credential Stuffing


Description

Detects credential stuffing attacks against OpenAI accounts by tracking the number of distinct source IP addresses submitting failed login attempts against the same email address within a short timeframe. Unlike brute force from a single IP, credential stuffing distributes attempts across many IPs to evade rate limiting. This rule complements OpenAI.BruteForce.Login.Success.Group, which confirms account compromise once a successful login follows the failures.

Query · python

def rule(event):
    return event.get("type") == "login.failed"


def unique(event):
    return event.deep_get("actor", "session", "ip_address", default="UNKNOWN_IP")


def dedup(event):
    return event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL")


def title(event):
    email = event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL")
    return f"[OpenAI] Credential stuffing detected against account [{email}]"


def alert_context(event):
    return {
        "email": event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL"),
        "ip_address": event.deep_get("actor", "session", "ip_address", default="UNKNOWN_IP"),
        "error_code": event.deep_get("login_failed", "error_code", default="UNKNOWN"),
    }

Analyst notes

  1. Review the distinct source IPs from the failed login attempts and check if they belong to known anonymization infrastructure (VPNs, Tor exit nodes, residential proxies) using threat intelligence
  2. Query OpenAI audit logs for all login.succeeded and login.failed events against the targeted email address within the past 30 minutes to identify all contributing source IPs and determine if any attempt succeeded — the alert context shows only the IP of the final triggering event, not all contributing IPs
  3. Check for other alerts involving the targeted email address in the past 7 days and look for login.succeeded events from unfamiliar locations or devices to assess whether account compromise has already occurred
Raw source OpenAI Credential Stuffing · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
RuleID: "OpenAI.CredentialStuffing"
DisplayName: "OpenAI Credential Stuffing"
Filename: openai_credential_stuffing.py
Enabled: true
LogTypes:
  - OpenAI.Audit
Severity: Medium
Threshold: 5
DedupPeriodMinutes: 30
Description: >
  Detects credential stuffing attacks against OpenAI accounts by tracking the number of
  distinct source IP addresses submitting failed login attempts against the same email
  address within a short timeframe. Unlike brute force from a single IP, credential
  stuffing distributes attempts across many IPs to evade rate limiting. This rule
  complements OpenAI.BruteForce.Login.Success.Group, which confirms account compromise
  once a successful login follows the failures.
Runbook: |
  1. Review the distinct source IPs from the failed login attempts and check if they belong to known anonymization infrastructure (VPNs, Tor exit nodes, residential proxies) using threat intelligence
  2. Query OpenAI audit logs for all login.succeeded and login.failed events against the targeted email address within the past 30 minutes to identify all contributing source IPs and determine if any attempt succeeded — the alert context shows only the IP of the final triggering event, not all contributing IPs
  3. Check for other alerts involving the targeted email address in the past 7 days and look for login.succeeded events from unfamiliar locations or devices to assess whether account compromise has already occurred
Reference: https://platform.openai.com/docs/api-reference/audit-logs
Tags:
  - OpenAI
  - Credential Access
Reports:
  MITRE ATT&CK:
    - TA0006:T1110.004 # Credential Access: Credential Stuffing
Tests:
  - Name: Match - Failed Login
    ExpectedResult: true
    Log:
      id: "audit_log-failed001"
      type: "login.failed"
      effective_at: 1702857600
      object: "organization.audit_log"
      actor:
        type: "session"
        session:
          user:
            id: "user-123"
            email: "user@company.com"
          ip_address: "203.0.113.100"
          user_agent: "Mozilla/5.0"
      login_failed:
        error_code: "invalid_credentials"
        error_message: "Invalid username or password"
  - Name: Match - Failed Login Different IP
    ExpectedResult: true
    Log:
      id: "audit_log-failed002"
      type: "login.failed"
      effective_at: 1702857610
      object: "organization.audit_log"
      actor:
        type: "session"
        session:
          user:
            id: "user-123"
            email: "user@company.com"
          ip_address: "198.51.100.25"
          user_agent: "python-requests/2.28.0"
      login_failed:
        error_code: "invalid_credentials"
        error_message: "Invalid username or password"
  - Name: No Match - Successful Login
    ExpectedResult: false
    Log:
      id: "audit_log-success001"
      type: "login.succeeded"
      effective_at: 1702857620
      object: "organization.audit_log"
      actor:
        type: "session"
        session:
          user:
            id: "user-123"
            email: "user@company.com"
          ip_address: "203.0.113.100"
          user_agent: "Mozilla/5.0"
  - Name: No Match - Non-Login Event
    ExpectedResult: false
    Log:
      id: "audit_log-api001"
      type: "api_key.created"
      effective_at: 1702857700
      object: "organization.audit_log"
      actor:
        type: "session"
        session:
          user:
            id: "user-123"
            email: "user@company.com"
          ip_address: "203.0.113.100"
          user_agent: "Mozilla/5.0"


# ------ paired body: openai_credential_stuffing.py ------

def rule(event):
    return event.get("type") == "login.failed"


def unique(event):
    return event.deep_get("actor", "session", "ip_address", default="UNKNOWN_IP")


def dedup(event):
    return event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL")


def title(event):
    email = event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL")
    return f"[OpenAI] Credential stuffing detected against account [{email}]"


def alert_context(event):
    return {
        "email": event.deep_get("actor", "session", "user", "email", default="UNKNOWN_EMAIL"),
        "ip_address": event.deep_get("actor", "session", "ip_address", default="UNKNOWN_IP"),
        "error_code": event.deep_get("login_failed", "error_code", default="UNKNOWN"),
    }

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.