This article covers two common scenarios where security events should be logged immediately at the point a 403 condition is detected, ensuring Init Sentinel captures accurate context without generating noise.
Principle: Log at the Decision Point, Not at the Response Layer
A 403 response can originate from multiple layers, but Init Sentinel only cares about where the request is explicitly judged to be unauthorized.
Logging too late loses intent, while logging too early creates unnecessary noise.
Scenario 1: Permission Denied Inside permission_callback
The permission_callback is the first security gate of a REST route. If a request is denied here, it should never reach the business logic.
This makes it the ideal location to log a security event with a clear action and a stable endpoint identifier.
// Create post
register_rest_route('init-api/v1', '/create-post', [
'methods' => 'POST',
'callback' => 'init_rest_create_post',
'permission_callback' => function () {
if ( ! current_user_can('publish_posts') ) {
init_html_log_security_event(
'api/create-post',
'permission_denied_create_post',
403
);
return new WP_Error(
'rest_forbidden',
__('You do not have permission to create posts.', 'init'),
['status' => 403]
);
}
return true;
},
]);
In this example, the log is written at the exact moment the permission check fails, before WordPress generates the response.
Scenario 2: Permission Denied Inside the REST Callback
Not every endpoint can determine authorization at the permission_callback level. For resource-specific actions, permission often depends on the request parameters.
In these cases, logging should happen immediately inside the failed permission branch within the callback.
// Get post
function init_rest_get_post( WP_REST_Request $request ) {
$post_id = absint( $request->get_param('post_id') );
if ( ! $post_id || get_post_type( $post_id ) !== 'post' ) {
return new WP_REST_Response( [ 'message' => 'Invalid post ID.' ], 400 );
}
if ( ! current_user_can( 'edit_post', $post_id ) ) {
init_html_log_security_event(
'api/get-post',
'unauthorized_get_post',
403
);
return new WP_REST_Response( [ 'message' => 'Unauthorized' ], 403 );
}
$post = get_post( $post_id );
$data = [
'ID' => $post->ID,
'post_title' => $post->post_title,
'post_content' => $post->post_content,
'status' => $post->post_status,
'author' => $post->post_author,
'thumbnail' => get_the_post_thumbnail_url( $post_id, 'full' ),
];
return rest_ensure_response( $data );
}
Here, the log is recorded only after the system confirms that the user lacks permission for the specific post, avoiding unnecessary security noise.
Why Not Log at template_redirect or rest_pre_serve_request
Generic hooks such as template_redirect or rest_pre_serve_request do not carry enough semantic context about the intent of the request.
Logging at those layers often results in vague records that indicate a 403 occurred but provide no insight into why.
Choosing Meaningful Endpoint and Action Names
Endpoint and action values are not meant for display but for behavioral analysis. They should be stable, concise, and reflect business intent.
Names like permission_denied_create_post or unauthorized_get_post clearly describe the blocked action and remain useful over time.
Conclusion
Logging 403 errors at the correct decision point allows Init Sentinel to function as a behavioral observability layer rather than a simple error counter.
In the next article, we will cover log rotation, cleanup strategies, and keeping the init_sentinel_security_log table performant at scale.
Comments