Gatekeeper inqaba-security/gatekeeper
v1.x

Enumeration & IDOR Detection

The siem.enum middleware detects resource enumeration — scripts walking through your object IDs (photos/1, photos/2, photos/3, ...) hunting for exposed data or Insecure Direct Object Reference (IDOR) holes. It's behavioural detection: it looks for access patterns no human browsing session produces.

Usage#

Attach to routes with parameters:

Route::get('/photos/{photo}', [PhotoController::class, 'show'])
    ->middleware('siem.enum');

Route::middleware('siem.enum')->group(function () {
    Route::get('/orders/{order}', ...);
    Route::get('/invoices/{invoice}', ...);
    Route::get('/users/{user}/profile', ...);
});

Routes without parameters are ignored (nothing to enumerate — plain 404 floods are abuse detection's job).

The three patterns#

Tracking is per IP + route pattern (photos/{photo}), inside a fixed 5-minute window:

1. sequential_scan — constant-stride ID walking#

Numeric IDs advancing by the same step: 1, 2, 3, 4, 5 (stride 1) or 10, 20, 30, 40 (stride 10). Five consecutive constant-stride hits (default) trigger the detection. Strides above max_stride (default 1000) don't count — jumping from ID 5 to 90210 isn't a scan step.

This is the classic dump-the-table script. Humans click links; they don't request arithmetically consecutive resources.

2. high_distinct_ids — abnormal breadth#

Fifteen distinct IDs (default) touched on one route pattern within the window — regardless of order or type, so it also catches shuffled scans and non-numeric identifiers (slugs, UUIDs). A human reads a few photos; a scraper reads every photo.

3. high_miss_ratio — guessing what isn't yours#

At least 50% of responses being 404/403/410 across ten or more requests (defaults). This is the IDOR-probing signature: the attacker doesn't know which IDs exist or which belong to them, so most guesses miss. Your authorization layer says "no" over and over — this detection makes the pattern of "no"s visible.

The event#

{
    "event": { "action": "resource_enumeration_detected", "severity": 7 },
    "threat": { "technique": { "id": ["T1119"], "name": ["Automated Collection"] } },
    "source": { "ip": "203.0.113.9" },
    "laravel": {
        "context": {
            "pattern": "sequential_scan",
            "resource": "photos/{photo}",
            "distinct_ids": 14,
            "sequential_run": 9,
            "stride": 1,
            "requests_in_window": 14,
            "miss_count": 11,
            "window_minutes": 5,
            "sample_ids": ["6", "7", "8", "9", "10"],
            "auto_blocked": false
        }
    }
}

Everything an analyst needs: which resource, which pattern, how deep the scan got, and a sample of the probed IDs.

Noise control & windowing#

  • Each pattern fires once per IP per route pattern per window.
  • The window is fixed, not sliding — its expiry is set at the first request and doesn't refresh. This matters: a sliding window that resets its TTL on every hit would let a slow scanner keep the window open forever without counters ever resetting.
  • Grouping by route pattern (not URL) means photos/1 and photos/2 are the same bucket, but photos/{id} and orders/{id} are scored independently.

Auto-blocking#

SIEM_ENUM_AUTOBLOCK=true

Blocks the scanner for enumeration.block_minutes (default 60) via the blocklist, with reason enumeration:<pattern>.

Configuration#

Key Env Default
enumeration.window_minutes 5
enumeration.sequential_threshold SIEM_ENUM_SEQUENTIAL 5
enumeration.max_stride 1000
enumeration.distinct_threshold SIEM_ENUM_DISTINCT 15
enumeration.miss_ratio 0.5
enumeration.miss_min_requests 10
enumeration.auto_block SIEM_ENUM_AUTOBLOCK false
enumeration.block_minutes 60

Tuning tips#

  • Paginated APIs — clients legitimately walking ?page=1,2,3 are fine (query strings aren't route parameters), but if you paginate via route params (/photos/page/{n}), skip siem.enum on those routes or raise sequential_threshold.
  • Gallery-style UX — if "next photo" buttons produce sequential IDs from real users, raise sequential_threshold to a depth no human paging session reaches (e.g. 25), or switch those URLs to UUIDs and lean on high_distinct_ids instead.
  • This detects, it doesn't authorize. Enumeration detection tells you someone is probing for IDOR; it does not fix an IDOR. Keep authorization in policies — and treat a high_miss_ratio event as a prompt to check what the attacker found before you blocked them.