Background
PR #110 review surfaced the question: should the extracted REST handlers re-sanitize their input (`submitter_email`, `url`, `category`, `project_url`, etc.) on entry, even though every route's `args` block already declares a `sanitize_callback` (`sanitize_email` / `esc_url_raw` / `sanitize_text_field` / `sanitize_textarea_field`) that runs during REST dispatch?
Current convention across every extracted handler:
```
includes/handlers/
├── academic-collaboration.php
├── community-channel.php
├── deploy-translation.php
├── flush-opcache.php
├── link-translations.php
├── local-group.php
├── project-status.php
├── refer-community-project.php ← review flagged
├── refer-local-group.php ← review flagged
├── relationship.php
├── send-verification-code.php
├── submit-project-send-code.php
├── submit-project.php ← review flagged
├── team-member.php
├── translate.php
└── update-disposable-domains.php
```
None of them re-sanitize. They all trust the REST framework's `sanitize_callback` to have run by the time the handler sees the request — which is true under the standard `register_rest_route` dispatch path.
Question to settle
Should we flip the convention to defense-in-depth — re-sanitize on entry into the handler — and if so, apply it consistently across all 16 handlers?
Arguments for re-sanitizing:
- The REST args sanitize_callback only runs when the handler is invoked via REST dispatch. If the function is ever called directly (custom plugin code, AJAX shim, future refactor), the sanitisation chain is bypassed.
- The handler's public contract becomes self-contained — readers don't have to cross-reference the args block in functions.php to know what's safe.
- Consistent with WP-VIP / hardened-codebase conventions where every persistence sink and every outbound email is the last line of defense.
Arguments against:
- WP's REST framework is the standard ingress; bypassing it is a misuse pattern. Adding internal sanitisation suggests the function is callable from anywhere, which it isn't.
- Re-sanitising via `sanitize_email` / `esc_url_raw` etc. is idempotent (passing already-clean input through them returns the same value), so the runtime cost is small but the code noise per handler is real.
- 16 handlers × ~5 fields each = ~80 new lines of glue with no behavioural change under the normal call path.
Suggested scope (if accepted)
For each handler, immediately after the `function_exists` guards but before any logic that touches request values:
```php
$email = sanitize_email((string) $request['submitter_email']);
$url = esc_url_raw((string) $request['url']);
$location = sanitize_text_field((string) $request['location']);
$description = sanitize_textarea_field((string) $request['description']);
// ... etc — only the fields the handler actually reads
```
Then replace every `$request['foo']` reference with the corresponding sanitised local. Tests would assert that the persisted post meta / outgoing emails contain the sanitised values, not the raw `$request` input.
Out of scope here
- Sanitising fields the handler doesn't read (no churn for parameters that flow only through the REST args block to never be touched again).
- Validation beyond sanitisation — `is_email()` etc. stay where they are.
- Plugin tree (`wordpress/plugins/cdcf-redis-translations/`) — its three handlers are simpler and don't read URL/email inputs.
Why this is a separate issue, not part of any single PR
Re-sanitisation is a cross-cutting decision. If we apply it to three of the sixteen handlers in isolation (which is what review on #110 effectively proposed), we get:
- Inconsistency between defensive and trusting handlers — future contributors don't know which pattern to follow.
- A "did we get them all" review burden each time a new handler lands.
Resolving the convention once across all 16 is cheaper than tracking the inconsistency over the long tail.
Related
Background
PR #110 review surfaced the question: should the extracted REST handlers re-sanitize their input (`submitter_email`, `url`, `category`, `project_url`, etc.) on entry, even though every route's `args` block already declares a `sanitize_callback` (`sanitize_email` / `esc_url_raw` / `sanitize_text_field` / `sanitize_textarea_field`) that runs during REST dispatch?
Current convention across every extracted handler:
```
includes/handlers/
├── academic-collaboration.php
├── community-channel.php
├── deploy-translation.php
├── flush-opcache.php
├── link-translations.php
├── local-group.php
├── project-status.php
├── refer-community-project.php ← review flagged
├── refer-local-group.php ← review flagged
├── relationship.php
├── send-verification-code.php
├── submit-project-send-code.php
├── submit-project.php ← review flagged
├── team-member.php
├── translate.php
└── update-disposable-domains.php
```
None of them re-sanitize. They all trust the REST framework's `sanitize_callback` to have run by the time the handler sees the request — which is true under the standard `register_rest_route` dispatch path.
Question to settle
Should we flip the convention to defense-in-depth — re-sanitize on entry into the handler — and if so, apply it consistently across all 16 handlers?
Arguments for re-sanitizing:
Arguments against:
Suggested scope (if accepted)
For each handler, immediately after the `function_exists` guards but before any logic that touches request values:
```php
$email = sanitize_email((string) $request['submitter_email']);
$url = esc_url_raw((string) $request['url']);
$location = sanitize_text_field((string) $request['location']);
$description = sanitize_textarea_field((string) $request['description']);
// ... etc — only the fields the handler actually reads
```
Then replace every `$request['foo']` reference with the corresponding sanitised local. Tests would assert that the persisted post meta / outgoing emails contain the sanitised values, not the raw `$request` input.
Out of scope here
Why this is a separate issue, not part of any single PR
Re-sanitisation is a cross-cutting decision. If we apply it to three of the sixteen handlers in isolation (which is what review on #110 effectively proposed), we get:
Resolving the convention once across all 16 is cheaper than tracking the inconsistency over the long tail.
Related