Skip to content

Commit f7ea514

Browse files
authored
Added support for \aikido\should_whitelist_request API + tests + docs (#404)
1 parent 96ecd3e commit f7ea514

File tree

37 files changed

+514
-34
lines changed

37 files changed

+514
-34
lines changed

docs/should_whitelist_request.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Should whitelist request
2+
3+
The `\aikido\should_whitelist_request` function allows the protected app to check whether the current request is whitelisted based on IP configuration. This can be used to skip custom security checks or apply special handling for requests coming from trusted or configured IPs.
4+
5+
## Function signature
6+
7+
```php
8+
AikidoWhitelistRequestStatus \aikido\should_whitelist_request()
9+
```
10+
11+
Returns an `AikidoWhitelistRequestStatus` object with the following properties:
12+
13+
| Property | Type | Description |
14+
|---------------|--------|---------------------------------------------------------------------|
15+
| `whitelisted` | bool | Whether the request is whitelisted. Defaults to `false`. |
16+
| `type` | string | The type of whitelist that matched. Empty string if not whitelisted.|
17+
| `trigger` | string | What triggered the whitelist (e.g., `"ip"`). Empty if not whitelisted. |
18+
| `description` | string | A human-readable description of why the request is whitelisted. |
19+
| `ip` | string | The IP address of the request. Empty if not whitelisted. |
20+
21+
## Whitelist types
22+
23+
The function checks three conditions in order. The first match wins:
24+
25+
1. **`endpoint-allowlist`** — The endpoint has a route-level IP allowlist configured and the request IP is in it. This indicates that IP-based access control is active for this route.
26+
2. **`bypassed`** — The request IP is in the global firewall bypass list.
27+
3. **`allowlist`** — The request IP is found in the global allowed IP list (e.g., geo-location allow lists).
28+
29+
If none of the above conditions match, `whitelisted` is `false` and all other fields are empty strings.
30+
31+
## Example
32+
33+
```php
34+
<?php
35+
36+
if (extension_loaded('aikido')) {
37+
$decision = \aikido\should_whitelist_request();
38+
39+
if ($decision->whitelisted) {
40+
// The request is whitelisted — skip custom security checks
41+
// $decision->type contains the reason: "endpoint-allowlist", "bypassed", or "allowlist"
42+
// $decision->description has a human-readable explanation
43+
}
44+
}
45+
```

lib/API.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ enum EVENT_ID {
1010
EVENT_REGISTER_PARAM_MATCHER,
1111
EVENT_GET_AUTO_BLOCKING_STATUS,
1212
EVENT_GET_BLOCKING_STATUS,
13+
EVENT_GET_WHITELISTED_STATUS,
1314
EVENT_GET_IS_IP_BYPASSED,
1415
EVENT_PRE_OUTGOING_REQUEST,
1516
EVENT_POST_OUTGOING_REQUEST,

lib/agent/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ go 1.26.1
44

55
require (
66
github.com/stretchr/testify v1.9.0
7-
google.golang.org/grpc v1.79.2
7+
google.golang.org/grpc v1.79.3
88
google.golang.org/protobuf v1.36.10
99
)
1010

lib/agent/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
108108
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
109109
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
110110
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
111+
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
112+
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
111113
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
112114
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
113115
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=

lib/php-extension/Action.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ ACTION_STATUS Action::executeStore(json &event) {
4141
return CONTINUE;
4242
}
4343

44+
ACTION_STATUS Action::executeWhitelist(json &event) {
45+
whitelisted = true;
46+
type = event["type"];
47+
trigger = event["trigger"];
48+
description = event["description"];
49+
if (trigger == "ip") {
50+
ip = event["ip"];
51+
}
52+
return CONTINUE;
53+
}
54+
4455
ACTION_STATUS Action::executeBypassIp(json &event) {
4556
AIKIDO_GLOBAL(isIpBypassed) = true;
4657
return CONTINUE;
@@ -73,6 +84,8 @@ ACTION_STATUS Action::Execute(std::string &event) {
7384
return executeWarningMessage(eventJson);
7485
} else if (actionType == "bypassIp") {
7586
return executeBypassIp(eventJson);
87+
} else if (actionType == "whitelisted") {
88+
return executeWhitelist(eventJson);
7689
}
7790
return CONTINUE;
7891
}
@@ -83,6 +96,7 @@ bool Action::IsDetection(std::string &event) {
8396

8497
void Action::Reset() {
8598
block = false;
99+
whitelisted = false;
86100
type = "";
87101
trigger = "";
88102
description = "";
@@ -98,6 +112,10 @@ bool Action::Block() {
98112
return block;
99113
}
100114

115+
bool Action::Whitelisted() {
116+
return whitelisted;
117+
}
118+
101119
char *Action::Type() {
102120
return (char *)type.c_str();
103121
}

lib/php-extension/Aikido.cpp

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ ZEND_DECLARE_MODULE_GLOBALS(aikido)
55

66
PHP_MINIT_FUNCTION(aikido) {
77
LoadSystemEnvironment();
8-
8+
99
AIKIDO_GLOBAL(logger).Init();
1010

1111
AIKIDO_LOG_INFO("MINIT started!\n");
1212

1313
RegisterAikidoBlockRequestStatusClass();
14+
RegisterAikidoWhitelistRequestStatusClass();
1415

1516
if (AIKIDO_GLOBAL(disable) == true) {
1617
AIKIDO_LOG_INFO("MINIT finished earlier because AIKIDO_DISABLE is set to 1!\n");
@@ -74,7 +75,7 @@ static void aikido_do_request_shutdown() {
7475
AIKIDO_LOG_DEBUG("RSHUTDOWN finished earlier because AIKIDO_DISABLE is set to 1!\n");
7576
return;
7677
}
77-
78+
7879
DestroyAstToClean();
7980
AIKIDO_GLOBAL(phpLifecycle).RequestShutdown();
8081

@@ -100,22 +101,22 @@ PHP_RSHUTDOWN_FUNCTION(aikido) {
100101
}
101102

102103
// PHP function: \aikido\worker_rinit()
103-
// Because FrankenPHP doesn't call RINIT for each request in worker mode,
104+
// Because FrankenPHP doesn't call RINIT for each request in worker mode,
104105
// we need to call it manually at the start of each request.
105106
// Only works with FrankenPHP worker mode
106107
PHP_FUNCTION(worker_rinit) {
107108
ZEND_PARSE_PARAMETERS_NONE();
108-
109+
109110
// Only allow this function in FrankenPHP worker mode
110111
if (std::string(sapi_module.name) != "frankenphp" || !AIKIDO_GLOBAL(isWorkerMode)) {
111112
zend_throw_exception(
112113
GetFirewallDefaultExceptionCe(),
113114
"aikido\\worker_rinit() can only be called in FrankenPHP worker mode", 0);
114115
RETURN_FALSE;
115116
}
116-
117+
117118
aikido_do_request_init();
118-
119+
119120
RETURN_TRUE;
120121
}
121122

@@ -125,17 +126,17 @@ PHP_FUNCTION(worker_rinit) {
125126
// Only works with FrankenPHP worker mode
126127
PHP_FUNCTION(worker_rshutdown) {
127128
ZEND_PARSE_PARAMETERS_NONE();
128-
129+
129130
// Only allow this function in FrankenPHP worker mode
130131
if (std::string(sapi_module.name) != "frankenphp" || !AIKIDO_GLOBAL(isWorkerMode)) {
131132
zend_throw_exception(
132133
GetFirewallDefaultExceptionCe(),
133134
"aikido\\worker_rshutdown() can only be called in FrankenPHP worker mode", 0);
134135
RETURN_FALSE;
135136
}
136-
137+
137138
aikido_do_request_shutdown();
138-
139+
139140
RETURN_TRUE;
140141
}
141142

@@ -149,6 +150,7 @@ static const zend_function_entry ext_functions[] = {
149150
ZEND_NS_FE("aikido", set_user, arginfo_aikido_set_user)
150151
ZEND_NS_FE("aikido", should_block_request, arginfo_aikido_should_block_request)
151152
ZEND_NS_FE("aikido", auto_block_request, arginfo_aikido_auto_block_request)
153+
ZEND_NS_FE("aikido", should_whitelist_request, arginfo_aikido_should_whitelist_request)
152154
ZEND_NS_FE("aikido", set_token, arginfo_aikido_set_token)
153155
ZEND_NS_FE("aikido", set_rate_limit_group, arginfo_aikido_set_rate_limit_group)
154156
ZEND_NS_FE("aikido", register_param_matcher, arginfo_aikido_register_param_matcher)
@@ -172,6 +174,7 @@ PHP_GINIT_FUNCTION(aikido) {
172174
aikido_globals->laravelEnvLoaded = false;
173175
aikido_globals->checkedAutoBlock = false;
174176
aikido_globals->checkedShouldBlockRequest = false;
177+
aikido_globals->checkedWhitelistRequest = false;
175178
aikido_globals->isIpBypassed = false;
176179
aikido_globals->isWorkerMode = false;
177180
aikido_globals->globalAstToClean = nullptr;

lib/php-extension/HandleBypassedIp.cpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,15 @@ void InitIpBypassCheck() {
1515
}
1616
}
1717

18+
bool IsAikidoDisabled() {
19+
return AIKIDO_GLOBAL(disable) == true;
20+
}
21+
22+
bool IsIpBypassed() {
23+
return AIKIDO_GLOBAL(isIpBypassed);
24+
}
1825

1926
bool IsAikidoDisabledOrBypassed() {
20-
return AIKIDO_GLOBAL(disable) == true || AIKIDO_GLOBAL(isIpBypassed);
27+
return IsAikidoDisabled() || IsIpBypassed();
2128
}
2229

lib/php-extension/HandleShouldBlockRequest.cpp

Lines changed: 71 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
#include "Includes.h"
22

33
zend_class_entry *blockingStatusClass = nullptr;
4-
5-
// The checkedAutoBlock module global variable is used to check if auto_block_request function
6-
// has already been called, in order to avoid multiple calls to this function.
7-
// Accessed via AIKIDO_GLOBAL(checkedAutoBlock).
8-
9-
// The checkedShouldBlockRequest module global variable is used to check if should_block_request
10-
// function has already been called, in order to avoid multiple calls to this function.
11-
// Accessed via AIKIDO_GLOBAL(checkedShouldBlockRequest).
4+
zend_class_entry *whitelistStatusClass = nullptr;
125

136
bool CheckBlocking(EVENT_ID eventId, bool& checkedBlocking) {
147
if (checkedBlocking) {
@@ -31,6 +24,27 @@ bool CheckBlocking(EVENT_ID eventId, bool& checkedBlocking) {
3124
return false;
3225
}
3326

27+
bool CheckWhitelist(EVENT_ID eventId, bool& checkedWhitelist) {
28+
if (checkedWhitelist) {
29+
return true;
30+
}
31+
32+
ScopedTimer scopedTimer("check_whitelist", "aikido_op");
33+
34+
try {
35+
auto& requestProcessorInstance = AIKIDO_GLOBAL(requestProcessorInstance);
36+
auto& action = AIKIDO_GLOBAL(action);
37+
std::string output;
38+
requestProcessorInstance.SendEvent(eventId, output);
39+
action.Execute(output);
40+
checkedWhitelist = true;
41+
return true;
42+
} catch (const std::exception &e) {
43+
AIKIDO_LOG_ERROR("Exception encountered in processing get whitelist status event: %s\n", e.what());
44+
}
45+
return false;
46+
}
47+
3448
ZEND_FUNCTION(should_block_request) {
3549
if (AIKIDO_GLOBAL(sapi_name) == "cli") {
3650
AIKIDO_LOG_DEBUG("should_block_request called in CLI mode! Skipping...\n");
@@ -83,6 +97,43 @@ ZEND_FUNCTION(auto_block_request) {
8397
CheckBlocking(EVENT_GET_AUTO_BLOCKING_STATUS, AIKIDO_GLOBAL(checkedAutoBlock));
8498
}
8599

100+
ZEND_FUNCTION(should_whitelist_request) {
101+
if (AIKIDO_GLOBAL(sapi_name) == "cli") {
102+
AIKIDO_LOG_DEBUG("should_whitelist_request called in CLI mode! Skipping...\n");
103+
return;
104+
}
105+
106+
if (!whitelistStatusClass) {
107+
return;
108+
}
109+
110+
if (IsAikidoDisabled()) {
111+
return;
112+
}
113+
114+
object_init_ex(return_value, whitelistStatusClass);
115+
116+
if (!CheckWhitelist(EVENT_GET_WHITELISTED_STATUS, AIKIDO_GLOBAL(checkedWhitelistRequest))) {
117+
return;
118+
}
119+
120+
#if PHP_VERSION_ID >= 80000
121+
zend_object *obj = Z_OBJ_P(return_value);
122+
if (!obj) {
123+
return;
124+
}
125+
#else
126+
zval *obj = return_value;
127+
#endif
128+
129+
auto& action = AIKIDO_GLOBAL(action);
130+
zend_update_property_bool(whitelistStatusClass, obj, "whitelisted", sizeof("whitelisted") - 1, action.Whitelisted());
131+
zend_update_property_string(whitelistStatusClass, obj, "type", sizeof("type") - 1, action.Type());
132+
zend_update_property_string(whitelistStatusClass, obj, "trigger", sizeof("trigger") - 1, action.Trigger());
133+
zend_update_property_string(whitelistStatusClass, obj, "description", sizeof("description") - 1, action.Description());
134+
zend_update_property_string(whitelistStatusClass, obj, "ip", sizeof("ip") - 1, action.Ip());
135+
}
136+
86137
void RegisterAikidoBlockRequestStatusClass() {
87138
zend_class_entry ce;
88139
INIT_CLASS_ENTRY(ce, "AikidoBlockRequestStatus", NULL); // Register class without methods
@@ -95,3 +146,15 @@ void RegisterAikidoBlockRequestStatusClass() {
95146
zend_declare_property_string(blockingStatusClass, "ip", sizeof("ip") - 1, "", ZEND_ACC_PUBLIC);
96147
zend_declare_property_string(blockingStatusClass, "user_agent", sizeof("user_agent") - 1, "", ZEND_ACC_PUBLIC);
97148
}
149+
150+
void RegisterAikidoWhitelistRequestStatusClass() {
151+
zend_class_entry ce;
152+
INIT_CLASS_ENTRY(ce, "AikidoWhitelistRequestStatus", NULL); // Register class without methods
153+
whitelistStatusClass = zend_register_internal_class(&ce);
154+
155+
zend_declare_property_bool(whitelistStatusClass, "whitelisted", sizeof("whitelisted") - 1, 0, ZEND_ACC_PUBLIC);
156+
zend_declare_property_string(whitelistStatusClass, "type", sizeof("type") - 1, "", ZEND_ACC_PUBLIC);
157+
zend_declare_property_string(whitelistStatusClass, "trigger", sizeof("trigger") - 1, "", ZEND_ACC_PUBLIC);
158+
zend_declare_property_string(whitelistStatusClass, "description", sizeof("description") - 1, "", ZEND_ACC_PUBLIC);
159+
zend_declare_property_string(whitelistStatusClass, "ip", sizeof("ip") - 1, "", ZEND_ACC_PUBLIC);
160+
}

lib/php-extension/PhpLifecycle.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ void PhpLifecycle::RequestInit() {
2323
AIKIDO_GLOBAL(requestProcessorInstance).RequestInit();
2424
AIKIDO_GLOBAL(checkedAutoBlock) = false;
2525
AIKIDO_GLOBAL(checkedShouldBlockRequest) = false;
26+
AIKIDO_GLOBAL(checkedWhitelistRequest) = false;
2627
AIKIDO_GLOBAL(isIpBypassed) = false;
2728
InitIpBypassCheck();
2829
}

lib/php-extension/Utils.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ const char* GetEventName(EVENT_ID event) {
4848
return "GetAutoBlockingStatus";
4949
case EVENT_GET_BLOCKING_STATUS:
5050
return "GetBlockingStatus";
51+
case EVENT_GET_WHITELISTED_STATUS:
52+
return "GetWhitelistedStatus";
5153
case EVENT_GET_IS_IP_BYPASSED:
5254
return "GetIsIpBypassed";
5355
case EVENT_PRE_OUTGOING_REQUEST:

0 commit comments

Comments
 (0)