Anthropic Session Reuse Impossible Travel


Description

Detects successful Anthropic audit activity for the same user email from source IP addresses whose query-time geo-locations (via IP_LOCATION) span at least two countries, are separated by at least 500 km, and imply travel faster than 800 km/h within a short (~15-minute) lookback. Unlike login-only impossible travel, this rule covers any successful user-actor activity and can surface session cookie replay or concurrent session reuse when no new authentication events appear.

Query · esql

from logs-anthropic.audit-*
| where
    data_stream.dataset == "anthropic.audit" and
    event.outcome == "success" and
    anthropic.audit.actor.type == "user_actor" and
    user.email is not null and
    source.ip is not null
| IP_LOCATION geo = source.ip with { "properties": ["country_name", "city_name", "location"] }
| eval
    Esql.source_geo_lat = st_y(geo.location),
    Esql.source_geo_lon = st_x(geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(geo.country_name),
    Esql.event_id_values = values(event.id),
    Esql.event_action_values = values(event.action),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(geo.country_name),
    Esql.source_geo_city_name_values = values(geo.city_name),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.anthropic_audit_actor_type_values = values(anthropic.audit.actor.type),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by user.email
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep user.email, Esql.*

Investigation fields

Pivot points the source recommends for triage.

  • user.email
  • Esql.distance_km
  • Esql.travel_kmh
  • Esql.window_minutes
  • Esql.country_count
  • Esql.event_count
  • Esql.event_id_values
  • Esql.event_action_values
  • Esql.source_ip_values
  • Esql.source_geo_country_name_values
  • Esql.source_geo_city_name_values
  • Esql.first_lat
  • Esql.first_lon
  • Esql.last_lat
  • Esql.last_lon
  • Esql.user_agent_original_values
  • Esql.anthropic_audit_actor_type_values
  • Esql.timestamp_first_seen
  • Esql.timestamp_last_seen

Known false positives

  • Users on VPN or proxy egress that geo-resolves through a region distant from the user's physical location. Mobile clients on cellular networks that peer through regional hubs may geo-resolve differently than the user's location.
  • Cloud egress, split-tunnel, or dual-homed clients that present different public IPs for concurrent Anthropic sessions (for example browser and API tooling) can look like impossible travel when both resolve far apart.

Analyst notes

Investigating Anthropic Session Reuse Impossible Travel

Successful user_actor activity for the same email spans distant countries too quickly for travel. Unlike login-only impossible travel, this covers any successful audit action — useful when cookie replay or concurrent sessions produce distant activity without a matching login.

A-B-A returns may not alert; sort raw Timeline events. No nearby magic_link_login_succeeded / sso_login_succeeded with distant successful non-login actions strengthens a session-reuse hypothesis.

Escalate when countries differ, UAs diverge (browser vs curl/python), login events are absent, or admin/export actions appear on the distant IP. Close as FP for documented dual-homed VPN/cloud egress with consistent corporate UAs.

Possible investigation steps

  • Use Esql.country_count, distance/speed, and Esql.event_action_values to see whether the distant hop was privileged (exports, role changes) vs routine chat views.
  • Timeline-sort events; compare IP, geo, UA, and action. Concurrent browser + tooling on far-apart cloud egress often explains FP; identical session cookies across continents without login does not.
  • Check for nearby logins. Absence + privileged distant actions → treat as cookie replay until proven otherwise.
  • Pair with Anthropic Impossible Travel Login / auth-failure rules when takeover is suspected.

False positive analysis

  • Split-tunnel / dual-homed clients (browser + API tooling) and VPN geo noise are the main FPs — require ASN/UA corroboration.

Response and remediation

  • On suspected replay/compromise: revoke sessions, reset credentials/MFA, hunt concurrent sessions, and review compliance/export and role changes for the same user.email after the distant events.
Raw source Anthropic Session Reuse Impossible Travel · Elastic TOML
Esc
Published by elastic/detection-rules ↗, licensed under Elastic License 2.0 ↗. Reproduced here unmodified.
[metadata]
creation_date = "2026/09/16"
integration = ["anthropic"]
maturity = "production"
min_stack_comments = "ES|QL IP_LOCATION requires 9.5.0+. FIRST/LAST aggregations and st_distance require 9.4.0+."
min_stack_version = "9.5.0"
updated_date = "2026/09/21"

[rule]
author = ["Elastic"]
description = """
Detects successful Anthropic audit activity for the same user email from source IP addresses whose query-time
geo-locations (via IP_LOCATION) span at least two countries, are separated by at least 500 km, and imply travel
faster than 800 km/h within a short (~15-minute) lookback. Unlike login-only impossible travel, this rule covers any
successful user-actor activity and can surface session cookie replay or concurrent session reuse when no new
authentication events appear.
"""
false_positives = [
    """
    Users on VPN or proxy egress that geo-resolves through a region distant from the user's physical location. Mobile
    clients on cellular networks that peer through regional hubs may geo-resolve differently than the user's location.
    """,
    """
    Cloud egress, split-tunnel, or dual-homed clients that present different public IPs for concurrent Anthropic
    sessions (for example browser and API tooling) can look like impossible travel when both resolve far apart.
    """,
]
from = "now-16m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "Anthropic Session Reuse Impossible Travel"
note = """## Triage and analysis

### Investigating Anthropic Session Reuse Impossible Travel

Successful `user_actor` activity for the same email spans distant countries too quickly for travel. Unlike login-only
impossible travel, this covers any successful audit action — useful when cookie replay or concurrent sessions produce
distant activity without a matching login.

A-B-A returns may not alert; sort raw Timeline events. No nearby `magic_link_login_succeeded` /
`sso_login_succeeded` with distant successful non-login actions strengthens a session-reuse hypothesis.

Escalate when countries differ, UAs diverge (browser vs curl/python), login events are absent, or admin/export
actions appear on the distant IP. Close as FP for documented dual-homed VPN/cloud egress with consistent corporate
UAs.

#### Possible investigation steps

- Use `Esql.country_count`, distance/speed, and `Esql.event_action_values` to see whether the distant hop was
  privileged (exports, role changes) vs routine chat views.
- Timeline-sort events; compare IP, geo, UA, and action. Concurrent browser + tooling on far-apart cloud egress often
  explains FP; identical session cookies across continents without login does not.
- Check for nearby logins. Absence + privileged distant actions → treat as cookie replay until proven otherwise.
- Pair with **Anthropic Impossible Travel Login** / auth-failure rules when takeover is suspected.

### False positive analysis

- Split-tunnel / dual-homed clients (browser + API tooling) and VPN geo noise are the main FPs — require ASN/UA
  corroboration.

### Response and remediation

- On suspected replay/compromise: revoke sessions, reset credentials/MFA, hunt concurrent sessions, and review
  compliance/export and role changes for the same `user.email` after the distant events.
"""
references = [
    "https://platform.claude.com/docs/en/api/compliance/activities/list",
]
risk_score = 73
rule_id = "291fc461-0dc8-4583-be5e-929467b3dd2e"
severity = "high"
tags = [
    "Domain: GenAI",
    "Domain: Identity",
    "Platform: Anthropic",
    "Data Source: Anthropic Audit Logs",
    "Use Case: Identity and Access Audit",
    "Use Case: Threat Detection",
    "Resources: Investigation Guide",
    "Rule Type: ES|QL",
    "Tactic: Credential Access",
    "Tactic: Initial Access",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-anthropic.audit-*
| where
    data_stream.dataset == "anthropic.audit" and
    event.outcome == "success" and
    anthropic.audit.actor.type == "user_actor" and
    user.email is not null and
    source.ip is not null
| IP_LOCATION geo = source.ip with { "properties": ["country_name", "city_name", "location"] }
| eval
    Esql.source_geo_lat = st_y(geo.location),
    Esql.source_geo_lon = st_x(geo.location)
| where Esql.source_geo_lat is not null and Esql.source_geo_lon is not null
| stats
    Esql.first_lat = first(Esql.source_geo_lat, @timestamp),
    Esql.first_lon = first(Esql.source_geo_lon, @timestamp),
    Esql.last_lat = last(Esql.source_geo_lat, @timestamp),
    Esql.last_lon = last(Esql.source_geo_lon, @timestamp),
    Esql.event_count = count(*),
    Esql.country_count = count_distinct(geo.country_name),
    Esql.event_id_values = values(event.id),
    Esql.event_action_values = values(event.action),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(geo.country_name),
    Esql.source_geo_city_name_values = values(geo.city_name),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.anthropic_audit_actor_type_values = values(anthropic.audit.actor.type),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by user.email
| where Esql.event_count >= 2 and Esql.country_count >= 2
| eval
    Esql.p1 = to_geopoint(concat("POINT(", to_string(Esql.first_lon), " ", to_string(Esql.first_lat), ")")),
    Esql.p2 = to_geopoint(concat("POINT(", to_string(Esql.last_lon), " ", to_string(Esql.last_lat), ")"))
| eval
    Esql.distance_km = round(st_distance(Esql.p1, Esql.p2) / 1000.0, 0),
    Esql.window_minutes = date_diff("minute", Esql.timestamp_first_seen, Esql.timestamp_last_seen),
    Esql.travel_kmh = case(Esql.window_minutes > 0, round(Esql.distance_km * 60.0 / Esql.window_minutes, 0), null)
| where Esql.distance_km >= 500 and Esql.travel_kmh >= 800
| keep user.email, Esql.*
'''


[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1539"
name = "Steal Web Session Cookie"
reference = "https://attack.mitre.org/techniques/T1539/"


[rule.threat.tactic]
id = "TA0006"
name = "Credential Access"
reference = "https://attack.mitre.org/tactics/TA0006/"

[[rule.threat]]
framework = "MITRE ATT&CK"
[[rule.threat.technique]]
id = "T1078"
name = "Valid Accounts"
reference = "https://attack.mitre.org/techniques/T1078/"
[[rule.threat.technique.subtechnique]]
id = "T1078.004"
name = "Cloud Accounts"
reference = "https://attack.mitre.org/techniques/T1078/004/"


[rule.threat.tactic]
id = "TA0001"
name = "Initial Access"
reference = "https://attack.mitre.org/tactics/TA0001/"

[rule.alert_suppression]
group_by = ["user.email"]
duration = {value = 15, unit = "m"}
missing_fields_strategy = "suppress"

[rule.investigation_fields]
field_names = [
    "user.email",
    "Esql.distance_km",
    "Esql.travel_kmh",
    "Esql.window_minutes",
    "Esql.country_count",
    "Esql.event_count",
    "Esql.event_id_values",
    "Esql.event_action_values",
    "Esql.source_ip_values",
    "Esql.source_geo_country_name_values",
    "Esql.source_geo_city_name_values",
    "Esql.first_lat",
    "Esql.first_lon",
    "Esql.last_lat",
    "Esql.last_lon",
    "Esql.user_agent_original_values",
    "Esql.anthropic_audit_actor_type_values",
    "Esql.timestamp_first_seen",
    "Esql.timestamp_last_seen",
]

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.