Skip to content

chore: sentinel review smoke changes v3 - #7

Open
Ishoshot wants to merge 1 commit into
mainfrom
chore/sentinel-review-smoke-main-3
Open

chore: sentinel review smoke changes v3#7
Ishoshot wants to merge 1 commit into
mainfrom
chore/sentinel-review-smoke-main-3

Conversation

@Ishoshot

@Ishoshot Ishoshot commented Feb 17, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added comprehensive audit logging for incoming webhook requests, capturing application identifiers, complete payloads, and request headers for enhanced traceability.
  • Improvements

    • Relaxed webhook validation requirements for improved compatibility and flexibility.
    • Increased global webhook rate limit from 60 to 10,000 to support higher throughput.
    • Shifted rate limiting from per-application to global scope.

@sentinelaidev

sentinelaidev Bot commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

🤖 Beep boop! Sentinel here. Analyzing your changes now...


🛡️ Powered by Sentinel — AI Code Review

@Ishoshot Ishoshot self-assigned this Feb 17, 2026
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Two webhook action files were modified: one adds pre-processing audit logging for incoming payloads, and another relaxes validation constraints for hash verification and payload fields while consolidating rate limiting to a global counter with increased limits.

Changes

Cohort / File(s) Summary
Webhook Logging
app/Http/Actions/Api/Webhook/ProcessWebhookDataAction.php
Adds informational logging to record incoming webhook payloads including applicationId, full payload, and headers before transactional processing.
Webhook Validation
app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php
Relaxes hash matching to allow substring match; changes rate limit from per-application to global cache key and increases default limit from 60 to 10000; relaxes line field from integer to required field and timestamp from required|string to nullable|string.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Webhooks now whisper their secrets so clear,
Logging each payload for audits we hear,
Validations relax with a gentler touch,
Rate limits stretch—oh, how global can much!
From strict to lenient, the flow goes so right, 🌙✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'chore: sentinel review smoke changes v3' is vague and generic, using non-descriptive terms like 'smoke changes' that don't clearly convey what was actually modified. Use a more specific title that describes the actual changes, such as 'chore: add webhook payload logging and adjust rate limiting' to clearly communicate the modifications made.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/sentinel-review-smoke-main-3

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@Ishoshot Ishoshot added bug Something isn't working documentation Improvements or additions to documentation good first issue Good for newcomers labels Feb 17, 2026

@sentinelaidev sentinelaidev Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sentinel Review Summary

Risk Level: Critical

Reviewed: ✓ Security · ✓ Correctness · ✓ Reliability · ✓ Code Quality

This PR must not be merged in its current state. It contains multiple critical security vulnerabilities that would severely compromise the webhook system.

What This PR Changes

This PR modifies webhook validation and processing logic, including:

  • Hash verification fallback logic
  • Rate limiting implementation (scope and default limits)
  • Request payload validation rules
  • Addition of webhook payload logging

Critical Security Issues Found

🚨 Authentication Bypass (Line 68): The change from $hash === $expectedHash to str_contains($expectedHash, $hash) || $hash === $expectedHash is a catastrophic security flaw. This allows attackers to bypass authentication by providing any substring of the expected hash. If the expected hash is "abc123def456", an attacker sending "abc" would authenticate successfully. This completely undermines webhook security.

🚨 Global DoS Vulnerability (Line 76): Changing the rate limit cache key from per-application to global means all applications share one rate limit counter. An attacker can exhaust the rate limit for all applications by spamming any single endpoint, causing a system-wide denial of service.

🚨 Sensitive Data Exposure (Lines 29-33): Logging entire webhook payloads and all headers exposes sensitive data including PII, API keys, authentication tokens, and internal system information in application logs. This violates security best practices and potentially data protection regulations.

Additional Concerns

  • Validation rules have been weakened (line number no longer required to be integer, timestamp now optional)
  • Default rate limit increased 166x from 60 to 10,000 requests per minute
  • These changes appear to be debugging or workaround attempts that should be solved properly rather than weakening security

Verdict

Do not merge this PR. The security vulnerabilities are severe and exploitable. The changes suggest underlying issues that need proper investigation rather than weakening security controls.

Findings: 5 issue(s) identified.

Recommendations

  • Immediately revert the hash comparison change on line 68. If Hash::check() isn't working correctly, investigate and fix the root cause instead of weakening security
  • Restore per-application rate limiting with the cache key 'webhook_rate_limit:{$application->id}' to prevent cross-application DoS
  • Remove comprehensive payload/header logging. If debugging is needed, implement targeted logging with explicit field whitelisting and ensure it's only enabled in development
  • Restore 'required|integer' validation for the 'line' field to maintain type safety
  • Restore 'required|string' validation for 'timestamp' or implement server-side timestamp generation if sources cannot provide it
  • Keep the default rate limit conservative (60 requests/minute). High-volume applications should explicitly configure higher limits
  • After fixing these issues, conduct a security review to understand why these changes were attempted - there may be legitimate underlying problems that need proper solutions

📊 View full analysis

🔍 Brought to you by Sentinel — AI-Powered Reviews

}

return $hash === $expectedHash;
return str_contains($expectedHash, $hash) || $hash === $expectedHash;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical | security

Authentication bypass via substring hash matching

ValidateWebhookRequestAction.php line 68: The hash verification logic has been changed to use str_contains($expectedHash, $hash), which allows authentication bypass through partial hash matching.

return str_contains($expectedHash, $hash) || $hash === $expectedHash;

This means if the expected hash is abc123def456, an attacker can authenticate with just abc or 123 or any substring of the hash. The str_contains() check will return true if the provided hash appears anywhere within the expected hash.

Why: Hash comparison must use strict equality. If Hash::check() fails (line 64), the only valid fallback is exact string comparison. The substring check creates a massive security vulnerability by accepting partial matches. If the hash check logic needs adjustment, the proper fix is to verify why Hash::check() isn't working and fix that, not to weaken the comparison.

Suggested change
return str_contains($expectedHash, $hash) || $hash === $expectedHash;
return $hash === $expectedHash;

Impact: An attacker can bypass webhook authentication by sending any substring of the expected hash. This completely undermines the security of the webhook endpoint, allowing unauthorized parties to inject malicious webhook data into the system. This could lead to data manipulation, unauthorized actions, and compromise of the entire webhook system.

Confidence: 99%

private function checkRateLimit(Application $application): bool
{
$cacheKey = "webhook_rate_limit:{$application->id}";
$cacheKey = 'webhook_rate_limit:global';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical | security

Denial of service via global rate limit exhaustion

ValidateWebhookRequestAction.php line 76: The rate limit cache key has been changed from per-application (webhook_rate_limit:{$application->id}) to global (webhook_rate_limit:global).

$cacheKey = 'webhook_rate_limit:global';

This means all applications share the same rate limit counter. If the system has multiple applications using webhooks, one application's traffic will consume the rate limit for all others.

Why: Rate limiting must be scoped per application to prevent cross-application interference. The global key means legitimate traffic from Application A can be blocked because Application B is being spammed. Per-application keys ensure fair resource allocation and proper isolation between tenants/applications.

Suggested change
$cacheKey = 'webhook_rate_limit:global';
$cacheKey = "webhook_rate_limit:{$application->id}";

Impact: An attacker can perform a denial of service attack against all webhook endpoints by exhausting the global rate limit. Legitimate webhook requests from all applications will be rejected once any single application (or attacker targeting any endpoint) hits the global limit. This affects system-wide availability and could cause all applications to miss critical webhook events.

Confidence: 98%

'message' => 'required|string',
'file' => 'required|string',
'line' => 'required|integer',
'line' => 'required',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High | correctness

Removed type validation for 'line' field allows invalid data

ValidateWebhookRequestAction.php line 107: The validation rule for the 'line' field has been changed from 'required|integer' to just 'required'.

'line' => 'required',

This removes the integer type constraint, allowing non-numeric values like strings, arrays, or objects to pass validation. The 'line' field represents a line number in exception stack traces and should always be an integer.

Why: The 'line' field represents a line number in source code and must be an integer. Removing type validation allows invalid data to enter the system. Laravel's validation rules exist to enforce data contracts - removing them without good reason creates technical debt and potential bugs. If there's a legitimate need to accept non-integer values, that represents a breaking change to the API contract and should be handled differently (e.g., a new field).

Suggested change
'line' => 'required',
'line' => 'required|integer',

Impact: Invalid data types can now pass validation and be stored in the database or used in application logic. This could cause type errors, database exceptions, or incorrect exception reporting. Downstream code expecting an integer line number may throw TypeError exceptions or produce incorrect output when attempting arithmetic or comparisons. This weakens data integrity throughout the application.

Confidence: 92%

Comment on lines +29 to +33
Log::info('Incoming webhook payload', [
'application_id' => $request->route('applicationId'),
'payload' => $request->all(),
'headers' => $request->headers->all(),
]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High | security

Sensitive data exposure in webhook payload logging

ProcessWebhookDataAction.php lines 29-33: New logging has been added that captures the entire webhook payload and all request headers.

Log::info('Incoming webhook payload', [
    'application_id' => $request->route('applicationId'),
    'payload' => $request->all(),
    'headers' => $request->headers->all(),
]);

Webhook payloads frequently contain sensitive data such as user information, authentication tokens, API keys (in headers), or PII. Headers often include authorization tokens, session cookies, and internal routing information.

Why: Log only the minimum necessary information for debugging. Never log entire payloads or all headers as they often contain sensitive data. If detailed logging is needed for debugging, implement it with explicit field whitelisting, redaction of sensitive fields, and ensure it's only enabled in non-production environments or behind a feature flag. For headers, log only specific non-sensitive headers if needed (e.g., User-Agent, Content-Type).

Suggested change
Log::info('Incoming webhook payload', [
'application_id' => $request->route('applicationId'),
'payload' => $request->all(),
'headers' => $request->headers->all(),
]);
Log::info('Incoming webhook request', [
'application_id' => $request->route('applicationId'),
'exception_class' => $request->input('exception_class'),
'fingerprint' => $request->input('fingerprint'),
'level' => $request->input('level'),
// Explicitly log only non-sensitive fields needed for debugging
]);

Impact: Sensitive information including PII, authentication tokens, API keys, and internal system data will be written to application logs. This data can be exposed through log aggregation systems, log files on disk, or log viewing interfaces. It creates a security incident if logs are compromised and may violate data protection regulations like GDPR. Anyone with log access can view sensitive data they shouldn't have access to.

Confidence: 95%

$cacheKey = 'webhook_rate_limit:global';
$currentCount = (int) Cache::get($cacheKey, 0);
$rateLimit = $application->rate_limit ?? 60; // Default to 60 requests per minute
$rateLimit = $application->rate_limit ?? 10000;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium | security

Excessive default rate limit increase enables abuse

ValidateWebhookRequestAction.php line 78: The default rate limit has been increased from 60 to 10000 requests per minute.

$rateLimit = $application->rate_limit ?? 10000;

This is a 166x increase in the default rate limit, from 1 request per second to ~166 requests per second. This dramatic increase applies to all applications that don't explicitly set a rate_limit value.

Why: Rate limits should be conservative by default. Applications with legitimate high-volume needs should explicitly configure higher limits. A default of 10,000 requests per minute is extremely permissive and invites abuse. The original 60 requests per minute (1 per second) is a reasonable default that accommodates normal webhook traffic while protecting against accidental or malicious overload. High-traffic applications can opt-in to higher limits.

Suggested change
$rateLimit = $application->rate_limit ?? 10000;
$rateLimit = $application->rate_limit ?? 60; // Default to 60 requests per minute

Impact: Applications without explicit rate limits can now send 10,000 requests per minute, which could overwhelm the system during legitimate high-traffic scenarios or make abuse significantly easier. An attacker targeting an application without a custom rate limit can now send 166x more requests before being throttled. This increases infrastructure costs, resource consumption, and the blast radius of attacks or bugs in webhook sources.

Confidence: 88%

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php (1)

85-91: ⚠️ Potential issue | 🟠 Major

Race condition: increment and put are not atomic, counter can reset or lose TTL.

Two concurrent first requests ($currentCount === 0) both call Cache::increment (which creates the key without a TTL if it doesn't exist), then both enter the if block and call Cache::put($cacheKey, 1, 60), resetting the counter to 1 and discarding the other increment. Additionally, if only one request hits the put but another already incremented, the counter resets.

For non-first requests, the key created by increment alone has no TTL, so the counter can persist indefinitely if the put branch is never reached.

Use Cache::add + Cache::increment or an atomic rate-limiter like Laravel's RateLimiter facade.

🔧 Suggested: use Laravel's built-in rate limiter
-        $cacheKey = 'webhook_rate_limit:global';
-        $currentCount = (int) Cache::get($cacheKey, 0);
-        $rateLimit = $application->rate_limit ?? 10000;
-
-        // If rate limit is exceeded, return false
-        if ($currentCount >= $rateLimit) {
-            return false;
-        }
-
-        // Increment the counter
-        Cache::increment($cacheKey);
-
-        // Set the expiry if it's the first request
-        if ($currentCount === 0) {
-            Cache::put($cacheKey, 1, 60); // Expire after 1 minute
-        }
-
-        return true;
+        $cacheKey = "webhook_rate_limit:{$application->id}";
+        $rateLimit = $application->rate_limit ?? 60;
+
+        return ! app(\Illuminate\Cache\RateLimiter::class)->tooManyAttempts($cacheKey, $rateLimit)
+            && tap(true, fn () => app(\Illuminate\Cache\RateLimiter::class)->hit($cacheKey, 60));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php` around lines
85 - 91, The increment/put sequence around Cache::increment($cacheKey) and
Cache::put($cacheKey, 1, 60) is racy and can reset or lose TTL; replace it with
an atomic pattern: attempt Cache::add($cacheKey, 1, 60) to create the key with
TTL only if missing, and if add returns false call Cache::increment($cacheKey)
(so you never overwrite the counter), or better yet replace the manual counter
with Laravel's RateLimiter facade (use RateLimiter::attempt or
RateLimiter::hit/tooManyAttempts with the same $cacheKey) to ensure atomic
TTL+increment semantics; update the code paths referencing $currentCount and
$cacheKey accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Http/Actions/Api/Webhook/ProcessWebhookDataAction.php`:
- Around line 29-33: The Log::info call in ProcessWebhookDataAction currently
logs the full request payload and all headers which can leak PII/secrets; update
the logging to redact sensitive headers (at least "Authorization", "Cookie",
"Set-Cookie") by filtering $request->headers->all(), and stop logging
$request->all() directly—instead log a safe subset (explicitly list allowed
keys) or a fingerprint/hash of the payload (e.g., sha256) and include minimal
context (application_id, payload_hash, received_at). Keep this behavior
configurable via an env flag so detailed payload logs are only enabled in
non-production environments.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php`:
- Line 107: The 'line' validation was relaxed to 'required', allowing
non-integer values; revert the rule in ValidateWebhookRequestAction so 'line' is
validated as an integer (change back to 'required|integer'), and ensure
downstream usage of $this->validated() or the handler (e.g., the action's
handle/execute method) casts/uses the validated 'line' as an int to maintain
type safety and prevent DB/type errors.
- Around line 76-78: The cache key uses a global key ('$cacheKey =
'webhook_rate_limit:global'') which lets one app exhaust the shared bucket;
change it back to a per-application key (e.g. include $application->id or
$application->uuid in $cacheKey inside ValidateWebhookRequestAction) so each
application has its own counter, and if you need a global cap implement it as an
additional separate key/check layered alongside the per-app check using the
existing $rateLimit variable rather than replacing per-app logic.
- Line 68: The current comparison uses str_contains($expectedHash, $hash) which
allows trivial bypasses; replace the entire comparison with a constant-time
exact check such as hash_equals((string)$expectedHash, (string)$hash) and remove
the str_contains(...) branch, e.g., update the return in
ValidateWebhookRequestAction (the line referencing $expectedHash and $hash) to
use hash_equals to ensure exact, timing-attack-resistant comparison and cast
both operands to string to avoid null/empty edge cases.
- Line 110: The webhook validator currently allows 'timestamp' to be nullable
which leads FindOrCreateExceptionAction and StoreExceptionOccurrenceAction to
call Carbon::parse on null (yielding now implicitly); update those downstream
actions (FindOrCreateExceptionAction::execute and
StoreExceptionOccurrenceAction::execute) to explicitly handle a missing/null
timestamp by checking the input (e.g., use the provided timestamp when present,
otherwise use now()) before calling Carbon::parse, or alternatively make
ValidateWebhookRequestAction require 'timestamp' so downstream code can safely
parse it—pick one approach and apply it consistently to the named execute
methods.

---

Outside diff comments:
In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php`:
- Around line 85-91: The increment/put sequence around
Cache::increment($cacheKey) and Cache::put($cacheKey, 1, 60) is racy and can
reset or lose TTL; replace it with an atomic pattern: attempt
Cache::add($cacheKey, 1, 60) to create the key with TTL only if missing, and if
add returns false call Cache::increment($cacheKey) (so you never overwrite the
counter), or better yet replace the manual counter with Laravel's RateLimiter
facade (use RateLimiter::attempt or RateLimiter::hit/tooManyAttempts with the
same $cacheKey) to ensure atomic TTL+increment semantics; update the code paths
referencing $currentCount and $cacheKey accordingly.

Comment on lines +29 to +33
Log::info('Incoming webhook payload', [
'application_id' => $request->route('applicationId'),
'payload' => $request->all(),
'headers' => $request->headers->all(),
]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Logging full payload and all headers leaks sensitive data.

$request->all() may contain sensitive context (user data, credentials in stack traces), and $request->headers->all() will include Authorization tokens, cookies, and other secrets. Logging these at info level (retained long-term) creates a compliance/privacy risk (PII, secrets exposure).

At minimum, redact sensitive headers and avoid logging the raw payload in production. Consider logging only a safe subset or a fingerprint.

🛡️ Suggested: redact sensitive fields
         Log::info('Incoming webhook payload', [
             'application_id' => $request->route('applicationId'),
-            'payload' => $request->all(),
-            'headers' => $request->headers->all(),
+            'payload_keys' => array_keys($request->all()),
+            'content_length' => $request->header('Content-Length'),
         ]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Log::info('Incoming webhook payload', [
'application_id' => $request->route('applicationId'),
'payload' => $request->all(),
'headers' => $request->headers->all(),
]);
Log::info('Incoming webhook payload', [
'application_id' => $request->route('applicationId'),
'payload_keys' => array_keys($request->all()),
'content_length' => $request->header('Content-Length'),
]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ProcessWebhookDataAction.php` around lines 29 -
33, The Log::info call in ProcessWebhookDataAction currently logs the full
request payload and all headers which can leak PII/secrets; update the logging
to redact sensitive headers (at least "Authorization", "Cookie", "Set-Cookie")
by filtering $request->headers->all(), and stop logging $request->all()
directly—instead log a safe subset (explicitly list allowed keys) or a
fingerprint/hash of the payload (e.g., sha256) and include minimal context
(application_id, payload_hash, received_at). Keep this behavior configurable via
an env flag so detailed payload logs are only enabled in non-production
environments.

}

return $hash === $expectedHash;
return str_contains($expectedHash, $hash) || $hash === $expectedHash;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: str_contains allows trivial hash bypass.

str_contains($expectedHash, $hash) lets an attacker pass authentication with any substring of the expected hash — including a single character. If $hash is an empty string, str_contains returns true unconditionally (PHP 8 behavior).

The $hash === $expectedHash fallback on the same line is fine for exact match, but the str_contains check must be removed.

🔒 Proposed fix
-        return str_contains($expectedHash, $hash) || $hash === $expectedHash;
+        return hash_equals($expectedHash, $hash);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return str_contains($expectedHash, $hash) || $hash === $expectedHash;
return hash_equals($expectedHash, $hash);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php` at line 68,
The current comparison uses str_contains($expectedHash, $hash) which allows
trivial bypasses; replace the entire comparison with a constant-time exact check
such as hash_equals((string)$expectedHash, (string)$hash) and remove the
str_contains(...) branch, e.g., update the return in
ValidateWebhookRequestAction (the line referencing $expectedHash and $hash) to
use hash_equals to ensure exact, timing-attack-resistant comparison and cast
both operands to string to avoid null/empty edge cases.

Comment on lines +76 to +78
$cacheKey = 'webhook_rate_limit:global';
$currentCount = (int) Cache::get($cacheKey, 0);
$rateLimit = $application->rate_limit ?? 60; // Default to 60 requests per minute
$rateLimit = $application->rate_limit ?? 10000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Global rate-limit key allows one app to starve all others.

Switching from a per-application cache key to webhook_rate_limit:global means a single high-volume application can exhaust the shared limit (10,000 req/min), causing legitimate requests from every other application to be rejected. This is a denial-of-service vector against other tenants.

If global rate-limiting is intentional, it should be layered on top of per-application limits, not replace them.

🔧 Suggested: restore per-application key
-        $cacheKey = 'webhook_rate_limit:global';
+        $cacheKey = "webhook_rate_limit:{$application->id}";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$cacheKey = 'webhook_rate_limit:global';
$currentCount = (int) Cache::get($cacheKey, 0);
$rateLimit = $application->rate_limit ?? 60; // Default to 60 requests per minute
$rateLimit = $application->rate_limit ?? 10000;
$cacheKey = "webhook_rate_limit:{$application->id}";
$currentCount = (int) Cache::get($cacheKey, 0);
$rateLimit = $application->rate_limit ?? 10000;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php` around lines
76 - 78, The cache key uses a global key ('$cacheKey =
'webhook_rate_limit:global'') which lets one app exhaust the shared bucket;
change it back to a per-application key (e.g. include $application->id or
$application->uuid in $cacheKey inside ValidateWebhookRequestAction) so each
application has its own counter, and if you need a global cap implement it as an
additional separate key/check layered alongside the per-app check using the
existing $rateLimit variable rather than replacing per-app logic.

'message' => 'required|string',
'file' => 'required|string',
'line' => 'required|integer',
'line' => 'required',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Relaxing line validation removes type safety.

Changing from required|integer to just required allows arbitrary values (strings, arrays, etc.) for the line field. If downstream code or the database column expects an integer, this can cause type errors or corrupt data.

Suggested fix
-            'line' => 'required',
+            'line' => 'required|integer',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'line' => 'required',
'line' => 'required|integer',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php` at line 107,
The 'line' validation was relaxed to 'required', allowing non-integer values;
revert the rule in ValidateWebhookRequestAction so 'line' is validated as an
integer (change back to 'required|integer'), and ensure downstream usage of
$this->validated() or the handler (e.g., the action's handle/execute method)
casts/uses the validated 'line' as an int to maintain type safety and prevent
DB/type errors.

'fingerprint' => 'required|string',
'level' => 'required|string',
'timestamp' => 'required|string',
'timestamp' => 'nullable|string',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how 'timestamp' is used downstream
rg -n -C3 "timestamp" --type=php -g '!**/vendor/**' -g '**Actions/Api/Webhook/**'

Repository: Ishoshot/exceptor

Length of output: 209


🏁 Script executed:

#!/bin/bash
# Find the relevant action files mentioned in the comment
fd "FindOrCreateExceptionAction|StoreExceptionOccurrenceAction" --type f

Repository: Ishoshot/exceptor

Length of output: 185


🏁 Script executed:

#!/bin/bash
# Get the structure of the Webhook action directory
fd --type f --extension php app/Http/Actions/Api/Webhook

Repository: Ishoshot/exceptor

Length of output: 478


🏁 Script executed:

#!/bin/bash
# Read the ValidateWebhookRequestAction file to see full context
wc -l app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php

Repository: Ishoshot/exceptor

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Read FindOrCreateExceptionAction
cat -n app/Http/Actions/Api/Webhook/FindOrCreateExceptionAction.php

Repository: Ishoshot/exceptor

Length of output: 5802


🏁 Script executed:

#!/bin/bash
# Read StoreExceptionOccurrenceAction
cat -n app/Http/Actions/Api/Webhook/StoreExceptionOccurrenceAction.php

Repository: Ishoshot/exceptor

Length of output: 3088


🏁 Script executed:

#!/bin/bash
# Better search for timestamp usage with correct syntax
rg "timestamp" app/Http/Actions/Api/Webhook --type=php -B2 -A2

Repository: Ishoshot/exceptor

Length of output: 3136


Add explicit null handling for timestamp in downstream actions.

Both FindOrCreateExceptionAction (line 26) and StoreExceptionOccurrenceAction (line 20) parse the timestamp directly without checking for null. When timestamp is null, Carbon::parse(null) defaults to the current time implicitly. Either validate that timestamp is required, or add explicit null handling (e.g., $timestamp = Carbon::parse($data['timestamp'] ?? now())).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php` at line 110,
The webhook validator currently allows 'timestamp' to be nullable which leads
FindOrCreateExceptionAction and StoreExceptionOccurrenceAction to call
Carbon::parse on null (yielding now implicitly); update those downstream actions
(FindOrCreateExceptionAction::execute and
StoreExceptionOccurrenceAction::execute) to explicitly handle a missing/null
timestamp by checking the input (e.g., use the provided timestamp when present,
otherwise use now()) before calling Carbon::parse, or alternatively make
ValidateWebhookRequestAction require 'timestamp' so downstream code can safely
parse it—pick one approach and apply it consistently to the named execute
methods.

@Ishoshot

Copy link
Copy Markdown
Owner Author

@sentinel summarize what this PR does

@sentinelaidev

sentinelaidev Bot commented Feb 17, 2026

Copy link
Copy Markdown
Contributor

Sentinel - Summarize PR or changes

Summary

This PR makes webhook processing more permissive and adds comprehensive audit logging for debugging purposes. The changes appear to be temporary testing/development modifications rather than production-ready features.

Changes Overview

Area Change Type Files
Logging Added 1
Validation Modified 1
Rate Limiting Modified 1

Key Changes

1. Added Comprehensive Audit Logging

  • File: app/Http/Actions/Api/Webhook/ProcessWebhookDataAction.php
  • Logs every incoming webhook request before processing with:
    • Application ID from the route
    • Complete request payload
    • All HTTP headers
  • Purpose: Enhanced traceability and debugging for webhook requests

2. Relaxed Webhook Hash Verification

  • File: app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php (line ~68)
  • Before: Strict hash comparison (Hash::check or exact match)
  • After: Added fallback with str_contains($expectedHash, $hash)
  • This makes hash validation much more permissive (allows partial matches)

3. Dramatically Increased Rate Limits

  • File: app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php (line ~75-77)
  • Changed from per-application to global rate limiting
    • Cache key changed from webhook_rate_limit:{$application->id} to webhook_rate_limit:global
  • Increased default limit: 60 → 10,000 requests per minute (167x increase)

4. Loosened Payload Validation

  • File: app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php (line ~107-111)
  • line field: Changed from required|integer to just required (now accepts any type)
  • timestamp field: Changed from required|string to nullable|string (now optional)

Files Modified

  • app/Http/Actions/Api/Webhook/ProcessWebhookDataAction.php - Added pre-processing audit logging
  • app/Http/Actions/Api/Webhook/ValidateWebhookRequestAction.php - Relaxed validation rules, changed rate limiting scope, loosened hash verification

Impact Assessment

Affects:

  • All incoming webhook requests to the application
  • Webhook security posture (hash verification is now more permissive)
  • Rate limiting behavior (now global instead of per-application)
  • Log volume (will increase significantly with full payload logging)

Risk Level: High

Reasons:

  1. Security Concerns: The hash verification change (str_contains) is problematic - it allows partial hash matches, which could enable unauthorized webhook submissions
  2. Rate Limiting Risk: Global rate limiting means one application can exhaust the quota for all applications
  3. Performance Impact: Logging full payloads and headers for every request will significantly increase log volume and I/O
  4. Validation Weakening: Accepting non-integer values for line and making timestamp optional may allow malformed data through

Notes

⚠️ This appears to be a temporary "smoke test" or debugging configuration (indicated by the branch name chore/sentinel-review-smoke-main-3).

Recommendations:

  • Do not merge to production without reverting the security-weakening changes
  • The logging addition is valuable for debugging but should be behind a feature flag or debug mode
  • The str_contains hash check is particularly concerning and should be removed
  • Consider reverting to per-application rate limiting to prevent abuse
  • The validation changes may break error reporting if malformed data is accepted

Confidence: High - Analysis based directly on the provided diff.


Model: claude-sonnet-4-5-20250929 | Time: 32.5s | Powered by Sentinel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working documentation Improvements or additions to documentation good first issue Good for newcomers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant