@raishin/vanguard-frontier-agentic 3.1.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +7 -1
- package/.cursor-plugin/plugin.json +7 -1
- package/.github/plugin/marketplace.json +1 -1
- package/README.md +11 -11
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/AGENT.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/claude-code.agent.md +111 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/codex.toml +37 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/copilot.agent.md +120 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/cursor.agent.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/gemini.agent.md +112 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/harnesses/kiro-ide.agent.md +111 -0
- package/agents/cross-functional/revenue-critical-journey-integrity-agent/metadata.json +42 -0
- package/agents/php/composer-supply-chain-agent/AGENT.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/claude-code.agent.md +113 -0
- package/agents/php/composer-supply-chain-agent/harnesses/codex.toml +119 -0
- package/agents/php/composer-supply-chain-agent/harnesses/copilot.agent.md +122 -0
- package/agents/php/composer-supply-chain-agent/harnesses/cursor.agent.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/gemini.agent.md +114 -0
- package/agents/php/composer-supply-chain-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/composer-supply-chain-agent/harnesses/kiro-ide.agent.md +113 -0
- package/agents/php/composer-supply-chain-agent/metadata.json +31 -0
- package/agents/php/php-application-security-agent/AGENT.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/claude-code.agent.md +112 -0
- package/agents/php/php-application-security-agent/harnesses/codex.toml +118 -0
- package/agents/php/php-application-security-agent/harnesses/copilot.agent.md +121 -0
- package/agents/php/php-application-security-agent/harnesses/cursor.agent.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/gemini.agent.md +113 -0
- package/agents/php/php-application-security-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-application-security-agent/harnesses/kiro-ide.agent.md +112 -0
- package/agents/php/php-application-security-agent/metadata.json +31 -0
- package/agents/php/php-maestro-agent/AGENT.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/claude-code.agent.md +80 -0
- package/agents/php/php-maestro-agent/harnesses/codex.toml +86 -0
- package/agents/php/php-maestro-agent/harnesses/copilot.agent.md +89 -0
- package/agents/php/php-maestro-agent/harnesses/cursor.agent.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/gemini.agent.md +81 -0
- package/agents/php/php-maestro-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-maestro-agent/harnesses/kiro-ide.agent.md +80 -0
- package/agents/php/php-maestro-agent/metadata.json +31 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/AGENT.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/claude-code.agent.md +116 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/codex.toml +122 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/copilot.agent.md +125 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/cursor.agent.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/gemini.agent.md +117 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/harnesses/kiro-ide.agent.md +116 -0
- package/agents/php/php-runtime-upgrade-readiness-agent/metadata.json +30 -0
- package/agents/php/wordpress-security-agent/AGENT.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/claude-code.agent.md +106 -0
- package/agents/php/wordpress-security-agent/harnesses/codex.toml +112 -0
- package/agents/php/wordpress-security-agent/harnesses/copilot.agent.md +115 -0
- package/agents/php/wordpress-security-agent/harnesses/cursor.agent.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/gemini.agent.md +107 -0
- package/agents/php/wordpress-security-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/php/wordpress-security-agent/harnesses/kiro-ide.agent.md +106 -0
- package/agents/php/wordpress-security-agent/metadata.json +31 -0
- package/catalog/agents.json +175 -0
- package/catalog/asset-integrity.json +503 -48
- package/catalog/install-roles.json +26 -4
- package/catalog/model-assignments.json +19811 -0
- package/catalog/model-policy.json +371 -0
- package/catalog/model-registry.json +134 -0
- package/catalog/skill-manifest.json +220 -18
- package/catalog/skills.json +163 -0
- package/package.json +7 -2
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/powers/README.md +3 -2
- package/powers/vanguard-generic/POWER.md +1 -1
- package/powers/vanguard-php/POWER.md +40 -0
- package/schemas/agent.schema.json +2 -1
- package/schemas/model-policy.schema.json +60 -0
- package/schemas/model-registry.schema.json +127 -0
- package/schemas/skill.schema.json +2 -1
- package/scripts/generate-docs-data.mjs +1 -1
- package/scripts/model-policy.mjs +1353 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/SKILL.md +108 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/metadata.json +29 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/idempotency-and-safe-retries.md +155 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/official-sources.md +71 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/pci-saq-scope-boundaries.md +118 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/server-side-revalidation-trust-boundary.md +134 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/webhook-delivery-dedup-ordering.md +146 -0
- package/skills/cross-functional/revenue-critical-journey-integrity-review/references/workflow-and-output.md +100 -0
- package/skills/gcp/gcp-firebase-developer/SKILL.md +1 -1
- package/skills/gcp/gcp-gke-platform-operator/SKILL.md +1 -1
- package/skills/php/composer-audit-supply-chain-review/SKILL.md +108 -0
- package/skills/php/composer-audit-supply-chain-review/metadata.json +20 -0
- package/skills/php/composer-audit-supply-chain-review/references/abandoned-and-advisory-governance.md +30 -0
- package/skills/php/composer-audit-supply-chain-review/references/composer-audit-policy.md +35 -0
- package/skills/php/composer-audit-supply-chain-review/references/lockfile-integrity.md +27 -0
- package/skills/php/php-maestro/SKILL.md +51 -0
- package/skills/php/php-maestro/metadata.json +20 -0
- package/skills/php/php-maestro/references/hard-gates-and-escalation.md +67 -0
- package/skills/php/php-maestro/references/routing-and-dispatch.md +91 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/SKILL.md +109 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/metadata.json +19 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/opcache-production-config.md +91 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-fpm-pool-tuning.md +87 -0
- package/skills/php/php-runtime-eol-opcache-fpm-review/references/php-version-lifecycle.md +102 -0
- package/skills/php/php-session-upload-deserialization-review/SKILL.md +111 -0
- package/skills/php/php-session-upload-deserialization-review/metadata.json +20 -0
- package/skills/php/php-session-upload-deserialization-review/references/file-upload-security.md +119 -0
- package/skills/php/php-session-upload-deserialization-review/references/session-security.md +126 -0
- package/skills/php/php-session-upload-deserialization-review/references/unserialize-object-injection.md +121 -0
- package/skills/php/wordpress-rest-block-security-review/SKILL.md +106 -0
- package/skills/php/wordpress-rest-block-security-review/metadata.json +20 -0
- package/skills/php/wordpress-rest-block-security-review/references/dynamic-block-output-escaping.md +42 -0
- package/skills/php/wordpress-rest-block-security-review/references/input-sanitize-output-escape.md +52 -0
- package/skills/php/wordpress-rest-block-security-review/references/rest-api-permission-callback.md +48 -0
- package/skills/salesforce/salesforce-agentforce-stdm-observer-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-apex-log-analyzer-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-apex-test-runner-skill/SKILL.md +1 -1
- package/skills/salesforce/salesforce-soql-explorer-skill/SKILL.md +1 -1
- package/tests/fixtures/php-maestro-routing/expected/001-happy-application-security.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/002-happy-composer-supply-chain.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/003-happy-runtime-upgrade-readiness.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/004-happy-wordpress-security.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/005-happy-unserialize-session.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/006-happy-fpm-opcache.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/007-happy-composer-audit.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/008-happy-wp-permission-callback.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-ambiguous.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-instruction-injection.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-db-migration-prod.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-liveguard-deploy-prod.json +4 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-persona-replacement.json +6 -0
- package/tests/fixtures/php-maestro-routing/expected/adv-secrets-bait.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/001-happy-application-security.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/002-happy-composer-supply-chain.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/003-happy-runtime-upgrade-readiness.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/004-happy-wordpress-security.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/005-happy-unserialize-session.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/006-happy-fpm-opcache.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/007-happy-composer-audit.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/008-happy-wp-permission-callback.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-ambiguous.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-instruction-injection.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-db-migration-prod.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-liveguard-deploy-prod.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-persona-replacement.json +7 -0
- package/tests/fixtures/php-maestro-routing/inputs/adv-secrets-bait.json +7 -0
- package/tests/fixtures/php-maestro-routing/taxonomy.json +69 -0
- package/tests/validate-catalog.py +1 -0
- package/tests/validate-skill-coherence.py +364 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: php-session-upload-deserialization-review
|
|
3
|
+
description: Use this skill to statically review PHP code for object-injection risk from unserialize() on untrusted input, session fixation/hijacking from missing session_regenerate_id() or weak session cookie hardening, and unsafe file-upload handling that trusts the client or stores/executes uploads inside the webroot. Use when reviewing a PHP application for deserialization, authentication-session, or upload-handling security issues. Static review only; it never executes payloads, uploads, or requests against any system, and every unserialize()/session/upload claim is grounded in the current php.net manual rather than memory.
|
|
4
|
+
allowed-tools: Read Grep Glob
|
|
5
|
+
metadata:
|
|
6
|
+
author: "github: Raishin"
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
updated: "2026-07-16"
|
|
9
|
+
category: security
|
|
10
|
+
lifecycle: experimental
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# PHP Session, Upload, and Deserialization Review
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Review PHP source for the three failure modes where the language's own deserialization, session, and file-upload primitives are used in a way php.net's own manual explicitly warns against: `unserialize()` called on untrusted input (object injection, potentially remote code execution), a session that is not regenerated after a privilege change or that ships without cookie hardening (fixation/hijacking), and an upload handler that trusts the client or stores/executes accepted files inside the webroot.
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
Use this skill when the user asks to:
|
|
22
|
+
|
|
23
|
+
- review any code path that calls `unserialize()`, or a library/cache/session backend that transparently unserializes stored data, for object-injection risk;
|
|
24
|
+
- review whether a login, role-elevation, or password-reset path correctly regenerates the session id and whether session cookie hardening (`use_strict_mode`, `httponly`, `secure`, `samesite`) is configured;
|
|
25
|
+
- review a file-upload handler for client-trust, storage-location, or size/type/count-limit gaps.
|
|
26
|
+
|
|
27
|
+
## When not to use
|
|
28
|
+
|
|
29
|
+
Do not use this skill for:
|
|
30
|
+
|
|
31
|
+
- designing or approving a backend authorization/permission model — this skill only checks whether a privilege change triggers session regeneration, not whether the privilege model itself is correct; hand that off to the owning backend/platform specialist.
|
|
32
|
+
- executing, crafting, or sending any deserialization payload, session-fixation request, or file upload against any live, sandbox, or staging system. This skill is static review only.
|
|
33
|
+
- issuing a compliance or audit attestation from an OWASP category mapping; the mapping is a classification aid, never a determination.
|
|
34
|
+
|
|
35
|
+
## Preconditions
|
|
36
|
+
|
|
37
|
+
- The code paths that call `unserialize()`, plus the source of the data passed to each call (request body/query/cookie, cache backend, queue message, database column).
|
|
38
|
+
- The authentication/session-management code, specifically where `session_start()`, `session_regenerate_id()`, and `$_SESSION` writes occur relative to a login, role change, or password reset.
|
|
39
|
+
- The active `session.*` INI settings in scope (`session.use_strict_mode`, `session.cookie_httponly`, `session.cookie_secure`, `session.cookie_samesite`) from `php.ini`, `.htaccess`, or runtime `ini_set()` calls.
|
|
40
|
+
- The upload-handling code: how `$_FILES` is validated, where accepted files are stored, and the `upload_max_filesize`/`post_max_size`/`max_file_uploads` configuration in effect.
|
|
41
|
+
- The PHP version in scope (`session.cookie_samesite` requires PHP 7.3+; destructor-on-fatal-error behavior changed at PHP 5.3.10).
|
|
42
|
+
|
|
43
|
+
## Lean operating rules
|
|
44
|
+
|
|
45
|
+
- Flag any `unserialize()` call on untrusted input regardless of `allowed_classes` — the manual's caution applies unconditionally, and `allowed_classes` narrows instantiable classes without making the call safe.
|
|
46
|
+
- Name the reachable magic-method surface (`__wakeup()`, `__unserialize()`, `__destruct()`) for any class an untrusted `unserialize()` call could instantiate; PHP invokes these automatically on the reconstructed object or during the shutdown sequence.
|
|
47
|
+
- Recommend `json_decode()`/`json_encode()` as the default remediation for untrusted data interchange unless serialized PHP objects are confirmed necessary.
|
|
48
|
+
- Require `session_regenerate_id(true)` before the authenticated flag is written to `$_SESSION` on every login/elevation/reset path; a regeneration call placed after that write is a finding, not a pass.
|
|
49
|
+
- Check `session.use_strict_mode`, `session.cookie_httponly`, `session.cookie_secure`, and `session.cookie_samesite` individually and explicitly; never infer hardening from defaults, since `use_strict_mode` is disabled by default.
|
|
50
|
+
- Require upload handlers to validate the file's actual content server-side (never the client-supplied `$_FILES[...]['type']` or filename alone), store accepted files outside any web-servable path, and enforce `upload_max_filesize`, `post_max_size`, and `max_file_uploads`.
|
|
51
|
+
- Never request, echo, store, or reproduce a secret, credential, session id, token, or PII found in reviewed code; redact-and-flag any such string.
|
|
52
|
+
- Label every claim `repo evidence` (seen in the reviewed code) or `documentation-based` (current php.net manual); never assert a documented API behavior from memory.
|
|
53
|
+
|
|
54
|
+
## Context7 documentation protocol
|
|
55
|
+
|
|
56
|
+
This skill's `allowed-tools` is limited to `Read`, `Grep`, and `Glob` — it does not include a live documentation-lookup tool, so grounding is done against the bundled, php.net-sourced reference files rather than a runtime Context7 call. `unserialize()`, the `session.*` INI directives, and the upload-handling behaviors reviewed here are core-language documentation, not a third-party library whose API shifts release to release, so the bundled references (each fetched directly from php.net, with its own Sources section and verification date) are the grounding source of record. If a review surfaces a claim about PHP behavior not covered by the bundled references — a different function, a different INI directive, a different PHP version's behavior — do not assert it from memory; say the claim is unverified against a specific php.net page and escalate for direct verification rather than guessing.
|
|
57
|
+
|
|
58
|
+
## Workflow
|
|
59
|
+
|
|
60
|
+
1. Enumerate every `unserialize()` call site in scope and trace the data source of its argument; classify each as untrusted-input-reachable or not.
|
|
61
|
+
2. For each untrusted-input-reachable `unserialize()` call, identify classes it could instantiate and check for `__wakeup()`, `__unserialize()`, and `__destruct()` magic methods that would fire automatically — see [Unserialize object injection](references/unserialize-object-injection.md).
|
|
62
|
+
3. For each authentication/privilege-elevation path, confirm `session_regenerate_id(true)` is called before `$_SESSION` carries the authenticated flag, and check the four session-hardening directives — see [Session security](references/session-security.md).
|
|
63
|
+
4. For each upload handler, confirm server-side content validation, non-webroot storage, and enforced size/type/count limits — see [File upload security](references/file-upload-security.md).
|
|
64
|
+
5. Emit findings with evidence tiers, exploit/failure narratives, remediation, verification steps, and OWASP category mappings; hand off any authorization-model or infrastructure-scoped issue to the owning specialist.
|
|
65
|
+
|
|
66
|
+
## Decision gates
|
|
67
|
+
|
|
68
|
+
- Block only on a demonstrated untrusted-input-reachable `unserialize()` call, a missing/misordered `session_regenerate_id()` call, a missing `session.use_strict_mode`, or an upload handler that trusts the client, stores inside the webroot, or lacks enforced limits.
|
|
69
|
+
- Every `unserialize()`, session-directive, or upload-limit claim cites the current php.net manual (`documentation-based`) or the reviewed code (`repo evidence`) — never memory.
|
|
70
|
+
- Every OWASP mapping names the specific edition and category cited and is labeled a classification aid, never a compliance determination.
|
|
71
|
+
- Every authorization-model-design or infrastructure-scoped finding is handed off, not adjudicated here.
|
|
72
|
+
|
|
73
|
+
## Evidence classification
|
|
74
|
+
|
|
75
|
+
Label each finding `repo evidence` (seen directly in the reviewed code or configuration) or `documentation-based` (the current php.net manual). This skill does not use `context7-grounded` (no Context7 tool in scope) and does not use `inference` for a documented API behavior — an undocumented or unverifiable behavior claim must be flagged as unverified rather than guessed.
|
|
76
|
+
|
|
77
|
+
## Security and privacy constraints
|
|
78
|
+
|
|
79
|
+
Static review only. Never request, transmit, store, or reproduce a secret, credential, session id, token, or PII found in reviewed code or configuration; treat any such string as a redact-and-flag finding. Never execute a deserialization payload, replay or forge a session id, upload a file, or send any request to a live, sandbox, or staging system. OWASP category mappings are classification aids, never compliance attestations.
|
|
80
|
+
|
|
81
|
+
## Escalation conditions
|
|
82
|
+
|
|
83
|
+
Escalate to incident response on any evidence the failure is already live: an object-injection payload observed in logs, a reachable uploaded web shell, or an observed session id reused across a privilege boundary. Escalate backend authorization-model design and infrastructure-scoped findings (webroot layout, TLS termination) to their owning specialist rather than adjudicating them here.
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
Load these only when needed:
|
|
88
|
+
|
|
89
|
+
- [Unserialize object injection](references/unserialize-object-injection.md) — why `unserialize()` on untrusted input is unsafe regardless of `allowed_classes`, the magic-method attack surface, and the `json_decode()` remediation.
|
|
90
|
+
- [Session security](references/session-security.md) — `session_regenerate_id()` ordering around privilege changes and the four session cookie-hardening directives.
|
|
91
|
+
- [File upload security](references/file-upload-security.md) — client-trust, storage-location, and size/type/count-limit review criteria for upload handlers.
|
|
92
|
+
|
|
93
|
+
Each reference file carries its own Sources section citing the exact php.net (or OWASP) page backing its claims — there is no separate primary-source ledger file in this skill.
|
|
94
|
+
|
|
95
|
+
## Response minimum
|
|
96
|
+
|
|
97
|
+
Return, at minimum:
|
|
98
|
+
|
|
99
|
+
- the call site/path in scope and, per finding, the failure class (`object-injection` / `session-fixation-hijacking` / `unsafe-upload`) and evidence tier;
|
|
100
|
+
- the exploit or failure narrative (how the reachable input reaches a wrong outcome);
|
|
101
|
+
- concrete remediation and an exact verification step;
|
|
102
|
+
- the OWASP category mapping, labeled as a classification aid;
|
|
103
|
+
- any authorization-model or infrastructure-scoped handoffs and incident-response escalation.
|
|
104
|
+
|
|
105
|
+
## Anti-goals
|
|
106
|
+
|
|
107
|
+
- Do not treat `allowed_classes` as a mitigation for `unserialize()` on untrusted input; the manual's caution is unconditional.
|
|
108
|
+
- Do not design or approve a backend authorization/permission model; hand it off.
|
|
109
|
+
- Do not execute any payload, upload, or request against any live, sandbox, or staging system.
|
|
110
|
+
- Do not assert `unserialize()`, session-directive, or upload-limit behavior from memory; ground every such claim in the current php.net manual.
|
|
111
|
+
- Do not echo, store, or reproduce any secret, session id, token, or PII found during review.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "php-session-upload-deserialization-review",
|
|
3
|
+
"name": "PHP Session, Upload & Deserialization Security Review",
|
|
4
|
+
"type": "skill",
|
|
5
|
+
"provider": "php",
|
|
6
|
+
"harnesses": ["claude-code", "cursor", "codex", "gemini", "kiro", "other"],
|
|
7
|
+
"summary": "Skill for reviewing PHP application-security seams: user-reachable unserialize() object injection (allowed_classes is insufficient), session fixation/hijacking hardening (session_regenerate_id, use_strict_mode, cookie flags), and unsafe file-upload handling, each mapped to an OWASP category and the php.net-documented fix.",
|
|
8
|
+
"source_type": "original",
|
|
9
|
+
"official_docs": [
|
|
10
|
+
"https://www.php.net/manual/en/function.unserialize.php",
|
|
11
|
+
"https://www.php.net/manual/en/session.security.php",
|
|
12
|
+
"https://www.php.net/manual/en/features.file-upload.common-pitfalls.php",
|
|
13
|
+
"https://owasp.org/www-project-top-ten/"
|
|
14
|
+
],
|
|
15
|
+
"security_notes": "Static-review-only skill: Read/Grep/Glob, no execution and no network. Never runs deserialization payloads, uploads, or exploits against any live or staging system. Any credential- or PII-shaped string is a redact-and-flag finding, never echoed. Every unserialize()/session/upload claim is grounded in current php.net documentation, not memory.",
|
|
16
|
+
"last_verified": "2026-07-16",
|
|
17
|
+
"path": "skills/php/php-session-upload-deserialization-review",
|
|
18
|
+
"author": "github: Raishin",
|
|
19
|
+
"version": "0.1.0"
|
|
20
|
+
}
|
package/skills/php/php-session-upload-deserialization-review/references/file-upload-security.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# File upload security
|
|
2
|
+
|
|
3
|
+
## Why this matters
|
|
4
|
+
|
|
5
|
+
An upload handler is the one place in most PHP applications where a client
|
|
6
|
+
supplies content the server will store, and often later serve, on its own
|
|
7
|
+
filesystem. Every trust decision the manual warns against — believing the
|
|
8
|
+
client's stated MIME type, believing the client's filename, skipping the
|
|
9
|
+
configured size/count ceilings, or placing accepted content somewhere the
|
|
10
|
+
web server will execute it — turns a routine upload feature into a path for
|
|
11
|
+
an attacker to plant and run a web shell, exhaust server resources, or read
|
|
12
|
+
files the application never meant to expose.
|
|
13
|
+
|
|
14
|
+
## NORMATIVE: configuration ceilings gate every upload
|
|
15
|
+
|
|
16
|
+
Per the php.net manual's common-pitfalls guidance for file uploads:
|
|
17
|
+
|
|
18
|
+
- **`upload_max_filesize`** caps the size PHP will accept per file: "The
|
|
19
|
+
`MAX_FILE_SIZE` item cannot specify a file size greater than the file size
|
|
20
|
+
that has been set in the `upload_max_filesize` in the php.ini file. The
|
|
21
|
+
default is 2 megabytes." A form-field `MAX_FILE_SIZE` hint is a
|
|
22
|
+
client-side convenience only; the server-enforced ceiling is
|
|
23
|
+
`upload_max_filesize`.
|
|
24
|
+
- **`post_max_size`** must accommodate the request as a whole: "If
|
|
25
|
+
`post_max_size` is set too small, large files cannot be uploaded. Make
|
|
26
|
+
sure you set `post_max_size` large enough." A `post_max_size` smaller than
|
|
27
|
+
`upload_max_filesize` silently caps effective upload size below what
|
|
28
|
+
`upload_max_filesize` alone suggests.
|
|
29
|
+
- **`max_file_uploads`** bounds how many files one request can carry: "The
|
|
30
|
+
`max_file_uploads` configuration setting controls the maximum number of
|
|
31
|
+
files that can [be] uploaded in one request. If more files are uploaded
|
|
32
|
+
than the limit, then `$_FILES` will stop processing files once the limit
|
|
33
|
+
is reached" — silently, not with an error the handler is guaranteed to
|
|
34
|
+
check for.
|
|
35
|
+
- **`memory_limit`**, **`max_execution_time`**, and **`max_input_time`** must
|
|
36
|
+
also be sized for uploads: the manual specifically notes `max_input_time`
|
|
37
|
+
"sets the maximum time, in seconds, the script is allowed to receive
|
|
38
|
+
input; this includes file uploads. For large or multiple files, or users
|
|
39
|
+
on slower connections, the default of `60` seconds may be exceeded,"
|
|
40
|
+
causing an otherwise-valid upload to fail partway through.
|
|
41
|
+
|
|
42
|
+
A review must confirm these are set deliberately (not left at silent
|
|
43
|
+
defaults that either reject legitimate uploads or, in the case of
|
|
44
|
+
`max_file_uploads`, silently truncate a batch without the handler noticing).
|
|
45
|
+
|
|
46
|
+
## NORMATIVE: do not trust which file you operate on
|
|
47
|
+
|
|
48
|
+
The manual's common-pitfalls page states plainly: "Not validating which file
|
|
49
|
+
you operate on may mean that users can access sensitive information in
|
|
50
|
+
other directories." This is the manual's own framing of path-traversal and
|
|
51
|
+
client-trust risk in upload handling — a filename or path is untrusted input
|
|
52
|
+
like any other and must be validated, not built into a filesystem operation
|
|
53
|
+
directly. The manual separately cautions that "due to the large amount of
|
|
54
|
+
directory listing styles we cannot guarantee that files with exotic names
|
|
55
|
+
(like containing spaces) are handled properly," reinforcing that the
|
|
56
|
+
client-supplied filename cannot be relied on to behave predictably and
|
|
57
|
+
should not be used to construct a storage path or to make a security
|
|
58
|
+
decision (such as inferring file type from its extension).
|
|
59
|
+
|
|
60
|
+
## RECOMMENDATION: validate content, not client claims; store outside the webroot
|
|
61
|
+
|
|
62
|
+
The manual's guidance above establishes the principle — validate the file
|
|
63
|
+
you actually operate on — but does not itself specify a validation
|
|
64
|
+
algorithm or a storage location; the following are this skill's operating
|
|
65
|
+
recommendations applying that principle, to be checked as reviewer evidence
|
|
66
|
+
criteria below rather than cited as separate manual quotations:
|
|
67
|
+
|
|
68
|
+
- Treat `$_FILES[...]['type']` (client-reported MIME type) and
|
|
69
|
+
`$_FILES[...]['name']` (client-reported filename) as untrusted metadata
|
|
70
|
+
only. A security decision — what kind of file this is, whether it is
|
|
71
|
+
acceptable — must be made from the file's actual content on the server,
|
|
72
|
+
not from either client-supplied field.
|
|
73
|
+
- Store accepted uploads outside any path the web server will serve
|
|
74
|
+
directly, and generate the stored filename server-side rather than
|
|
75
|
+
reusing the client-supplied name verbatim, closing both the
|
|
76
|
+
path-traversal risk above and the risk of an executable extension being
|
|
77
|
+
preserved into a servable location.
|
|
78
|
+
- Never `include()`, `require()`, or otherwise execute an uploaded file as
|
|
79
|
+
PHP code, and never configure the storage directory to be
|
|
80
|
+
script-executable by the web server.
|
|
81
|
+
|
|
82
|
+
## Reviewer evidence criteria
|
|
83
|
+
|
|
84
|
+
For each upload handler in scope:
|
|
85
|
+
|
|
86
|
+
- Confirm `upload_max_filesize`, `post_max_size` (sized larger than
|
|
87
|
+
`upload_max_filesize`), and `max_file_uploads` are set to deliberate,
|
|
88
|
+
documented values rather than left at framework/PHP defaults without
|
|
89
|
+
review, and confirm the handler does not assume a client-side
|
|
90
|
+
`MAX_FILE_SIZE` hint is enforced.
|
|
91
|
+
- Confirm the handler checks the actual number of files processed against
|
|
92
|
+
what the client claims to have sent, given that `$_FILES` silently stops
|
|
93
|
+
populating once `max_file_uploads` is reached.
|
|
94
|
+
- Confirm the handler validates the file's real type/content server-side,
|
|
95
|
+
not `$_FILES[...]['type']` or the client filename's extension alone.
|
|
96
|
+
- Confirm accepted files are stored outside any web-servable path, with a
|
|
97
|
+
server-generated filename, not the client-supplied one used verbatim in
|
|
98
|
+
a filesystem path.
|
|
99
|
+
- Confirm no code path `include()`s, `require()`s, or otherwise executes an
|
|
100
|
+
uploaded file, and that the storage location is not configured as
|
|
101
|
+
script-executable.
|
|
102
|
+
- Treat trusting client-supplied MIME type/filename for a security decision,
|
|
103
|
+
storing inside the webroot, or having no enforced size/type/count limit as
|
|
104
|
+
a blocking finding.
|
|
105
|
+
|
|
106
|
+
## Applicable versions
|
|
107
|
+
|
|
108
|
+
The `upload_max_filesize` default (2 megabytes) and the `max_file_uploads`
|
|
109
|
+
silent-truncation behavior described above are current php.net manual
|
|
110
|
+
guidance as of this review; re-verify against the live manual page before
|
|
111
|
+
citing an exact default value, since PHP distribution defaults (and
|
|
112
|
+
distro-specific `php.ini` overrides) can differ from the documented
|
|
113
|
+
upstream default.
|
|
114
|
+
|
|
115
|
+
## Sources
|
|
116
|
+
|
|
117
|
+
- [PHP Manual — Common Pitfalls (file uploads)](https://www.php.net/manual/en/features.file-upload.common-pitfalls.php) — supports the `upload_max_filesize` default and `MAX_FILE_SIZE` client-hint framing, the `post_max_size` sizing guidance, the `max_file_uploads` silent-stop behavior, the `max_input_time` upload-duration caution, and the "not validating which file you operate on" and "exotic names" client-trust cautions.
|
|
118
|
+
|
|
119
|
+
Last verified: 2026-07-16.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Session security
|
|
2
|
+
|
|
3
|
+
## Why this matters
|
|
4
|
+
|
|
5
|
+
A session id is a bearer credential: whoever presents it is treated as the
|
|
6
|
+
user it belongs to. Two distinct failures let an attacker end up holding a
|
|
7
|
+
victim's session id: fixation, where the attacker sets the session id before
|
|
8
|
+
the victim authenticates and it is never replaced, and hijacking, where the
|
|
9
|
+
attacker steals an already-authenticated session id after the fact (via
|
|
10
|
+
script injection, packet sniffing, or physical access). php.net's own
|
|
11
|
+
session-security guidance treats both as core, not edge-case, concerns, and
|
|
12
|
+
documents specific configuration and code-ordering requirements to close
|
|
13
|
+
each path.
|
|
14
|
+
|
|
15
|
+
## NORMATIVE: regenerate the session id on privilege change
|
|
16
|
+
|
|
17
|
+
Per the php.net manual's session-management-basics guidance:
|
|
18
|
+
|
|
19
|
+
- "Session IDs **must** be regenerated when user privileges are elevated,
|
|
20
|
+
such as after authenticating. `session_regenerate_id()` must be called
|
|
21
|
+
prior to setting the authentication information to `$_SESSION`."
|
|
22
|
+
`session_regenerate_id()` saves the current session data automatically, so
|
|
23
|
+
the call itself does not lose session state — but it must come **before**
|
|
24
|
+
the authenticated flag is written, so that only the new session id carries
|
|
25
|
+
it.
|
|
26
|
+
- More generally: "Session ID regeneration reduces the risk of stolen
|
|
27
|
+
session IDs, thus `session_regenerate_id()` must be called periodically.
|
|
28
|
+
E.g. Regenerate the session ID every 15 minutes for security sensitive
|
|
29
|
+
content."
|
|
30
|
+
- `session_regenerate_id()` does **not** delete the outdated session's data
|
|
31
|
+
by default. The manual is explicit: "`session_regenerate_id()` does not
|
|
32
|
+
delete outdated sessions by default. Obsolete authenticated sessions may
|
|
33
|
+
be present for use. Developers must prevent outdated sessions to be
|
|
34
|
+
consumed by anyone." A regeneration call without also invalidating the
|
|
35
|
+
prior session's stored data leaves a still-valid, still-authenticated old
|
|
36
|
+
session id usable by anyone who captured it before regeneration.
|
|
37
|
+
|
|
38
|
+
## NORMATIVE: `session.use_strict_mode` is mandatory and off by default
|
|
39
|
+
|
|
40
|
+
Per the manual: "enabling `session.use_strict_mode` is mandatory for secure
|
|
41
|
+
sessions. It is disabled by default. This prevents the session module [from
|
|
42
|
+
using] an uninitialized session ID. Put differently, the session module only
|
|
43
|
+
accepts valid session IDs generated by the session module. It rejects any
|
|
44
|
+
session ID supplied by users." With strict mode enabled, "an uninitialized
|
|
45
|
+
session ID is rejected and a new one is created. This prevents an attack that
|
|
46
|
+
forces users to use a known session ID" — the core session-fixation vector,
|
|
47
|
+
where an attacker "may paste links or send emails that contain the session
|
|
48
|
+
ID" and hope the victim's session simply adopts it.
|
|
49
|
+
|
|
50
|
+
Because this directive is **disabled by default**, a review must check for
|
|
51
|
+
an explicit `session.use_strict_mode=1` (or equivalent `ini_set()` call) —
|
|
52
|
+
absence of an error or warning is not evidence it is on.
|
|
53
|
+
|
|
54
|
+
## NORMATIVE: cookie-hardening directives
|
|
55
|
+
|
|
56
|
+
Per the php.net manual's session INI-settings guidance, four directives
|
|
57
|
+
harden the session cookie itself:
|
|
58
|
+
|
|
59
|
+
- **`session.cookie_httponly`** — "Refuses access to the session cookie from
|
|
60
|
+
JavaScript. This setting prevents cookies snatched by a JavaScript
|
|
61
|
+
injection." The manual states: "Almost all applications must use the
|
|
62
|
+
httponly attribute for the session ID cookie."
|
|
63
|
+
- **`session.cookie_secure`** — "Allow access to the session ID cookie only
|
|
64
|
+
when the protocol is HTTPS. If a website is only accessible via HTTPS, it
|
|
65
|
+
should enable this setting." (The manual separately notes HSTS should be
|
|
66
|
+
considered for HTTPS-only sites.)
|
|
67
|
+
- **`session.cookie_samesite`** — "As of PHP 7.3 the `SameSite` attribute can
|
|
68
|
+
be set for the session ID cookie. This attribute is a way to mitigate CSRF
|
|
69
|
+
(Cross Site Request Forgery) attacks." `Lax` allows the cookie on a
|
|
70
|
+
cross-site GET navigation; `Strict` does not send it cross-site at all —
|
|
71
|
+
the manual frames the choice as that accessibility trade-off, not a
|
|
72
|
+
recommendation of one value for every application.
|
|
73
|
+
- Session hijacking generally (not fixation) is mitigated, per the manual,
|
|
74
|
+
by transport: "There are several ways to leak an existing session ID to
|
|
75
|
+
third parties. E.g. JavaScript injections, session IDs in URLs, packet
|
|
76
|
+
sniffing, physical access to the device, etc... The solution is to
|
|
77
|
+
implement SSL/TLS on the server and make it mandatory for users" —
|
|
78
|
+
`session.cookie_secure` is the configuration lever that enforces this for
|
|
79
|
+
the session cookie specifically.
|
|
80
|
+
|
|
81
|
+
## Reviewer evidence criteria
|
|
82
|
+
|
|
83
|
+
For every authentication, privilege-elevation, or password-reset path:
|
|
84
|
+
|
|
85
|
+
- Confirm `session_regenerate_id(true)` is called, and confirm it is called
|
|
86
|
+
**before** the code writes the authenticated/elevated-privilege flag into
|
|
87
|
+
`$_SESSION`. A call present anywhere in the function is not sufficient —
|
|
88
|
+
check the ordering.
|
|
89
|
+
- Confirm the prior session's data is invalidated or is not relied upon as
|
|
90
|
+
still-authenticated after regeneration; a regeneration call with the old
|
|
91
|
+
session id left independently valid and authenticated is a finding.
|
|
92
|
+
|
|
93
|
+
For the effective session configuration in scope (`php.ini`, `.htaccess`,
|
|
94
|
+
`ini_set()` calls, or a framework's session configuration layer):
|
|
95
|
+
|
|
96
|
+
- Confirm `session.use_strict_mode` is explicitly enabled; do not treat its
|
|
97
|
+
absence from configuration as acceptable, since it is disabled by default.
|
|
98
|
+
- Confirm `session.cookie_httponly` is enabled.
|
|
99
|
+
- Confirm `session.cookie_secure` is enabled for any application served over
|
|
100
|
+
HTTPS (and flag its absence as a finding for an HTTPS-only application).
|
|
101
|
+
- Confirm `session.cookie_samesite` is set to `Lax` or `Strict` (requires
|
|
102
|
+
PHP 7.3+); flag an unset value, and flag PHP < 7.3 as unable to set this
|
|
103
|
+
directive at all (a version-upgrade finding, not a configuration finding).
|
|
104
|
+
|
|
105
|
+
Treat a missing `session_regenerate_id()` call (or one ordered after the
|
|
106
|
+
authenticated flag is set) and a missing/disabled `session.use_strict_mode`
|
|
107
|
+
as blocking findings. Treat missing `cookie_httponly`, `cookie_secure`, or
|
|
108
|
+
`cookie_samesite` as findings that escalate to blocking when combined with
|
|
109
|
+
any independently confirmed session-id-leak or fixation path.
|
|
110
|
+
|
|
111
|
+
## Applicable versions
|
|
112
|
+
|
|
113
|
+
`session.cookie_samesite` requires PHP 7.3 or later — on an earlier version
|
|
114
|
+
this is a version-upgrade finding rather than a configuration omission. The
|
|
115
|
+
other directives and the `session_regenerate_id()` ordering guidance are
|
|
116
|
+
current php.net manual guidance as of this review; re-verify against the
|
|
117
|
+
live manual page before citing behavior for a PHP version outside current
|
|
118
|
+
support.
|
|
119
|
+
|
|
120
|
+
## Sources
|
|
121
|
+
|
|
122
|
+
- [PHP Manual — Session Management Basics](https://www.php.net/manual/en/features.session.security.management.php) — supports the `session_regenerate_id()` ordering requirement around privilege elevation, the periodic-regeneration guidance, and the "does not delete outdated sessions by default" caution.
|
|
123
|
+
- [PHP Manual — Securing Session INI Settings](https://www.php.net/manual/en/session.security.ini.php) — supports `session.use_strict_mode` being mandatory-but-disabled-by-default, `session.cookie_httponly`, `session.cookie_secure`, and `session.cookie_samesite` (PHP 7.3+) directive behavior.
|
|
124
|
+
- [PHP Manual — Sessions and Security (overview)](https://www.php.net/manual/en/session.security.php) — top-level page framing session security as core to web application security and pointing to the two subsection pages above.
|
|
125
|
+
|
|
126
|
+
Last verified: 2026-07-16.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Unserialize object injection
|
|
2
|
+
|
|
3
|
+
## Why this matters
|
|
4
|
+
|
|
5
|
+
`unserialize()` does not just decode data — it can rebuild arbitrary objects
|
|
6
|
+
and then automatically hand them control. If the serialized string an
|
|
7
|
+
attacker controls names a class the application has autoloaded (directly or
|
|
8
|
+
transitively), reconstructing that object can invoke its magic methods with
|
|
9
|
+
attacker-influenced property values, and one of those methods reaching a
|
|
10
|
+
dangerous sink (a file write, a command, a second `unserialize()`, a query)
|
|
11
|
+
is enough to turn a data-parsing call into code execution. This is PHP
|
|
12
|
+
object injection, and php.net's own manual treats it as unconditional: there
|
|
13
|
+
is no safe way to call `unserialize()` on untrusted input, only safer
|
|
14
|
+
alternatives to use instead.
|
|
15
|
+
|
|
16
|
+
## NORMATIVE: php.net's unconditional caution
|
|
17
|
+
|
|
18
|
+
Per the php.net manual page for `unserialize()`:
|
|
19
|
+
|
|
20
|
+
- "Do not pass untrusted user input to `unserialize()` regardless of the
|
|
21
|
+
`options` value of `allowed_classes`. Unserialization can result in code
|
|
22
|
+
being loaded and executed due to object instantiation and autoloading, and
|
|
23
|
+
a malicious user may be able to exploit this."
|
|
24
|
+
- The manual's stated alternative for untrusted data interchange is "a safe,
|
|
25
|
+
standard data interchange format such as JSON (via `json_decode()` and
|
|
26
|
+
`json_encode()`)."
|
|
27
|
+
- After successfully reconstructing an object, PHP "will automatically
|
|
28
|
+
attempt to call the `__unserialize()` or `__wakeup()` methods (if one
|
|
29
|
+
exists)" — this invocation happens as part of `unserialize()` itself, not
|
|
30
|
+
as a separate step a caller can skip.
|
|
31
|
+
|
|
32
|
+
This is the single controlling fact for this review: the caution is not
|
|
33
|
+
scoped to "when `allowed_classes` is absent" — it applies **regardless of**
|
|
34
|
+
that option's value. A review must not treat `allowed_classes` as the fix.
|
|
35
|
+
|
|
36
|
+
## RECOMMENDATION: what `allowed_classes` actually does, and does not do
|
|
37
|
+
|
|
38
|
+
`allowed_classes` restricts which class names `unserialize()` is willing to
|
|
39
|
+
instantiate (e.g. `unserialize($string, ['allowed_classes' => false])`
|
|
40
|
+
instantiates no objects at all, converting them to `__PHP_Incomplete_Class`
|
|
41
|
+
instead). Use it as defense-in-depth when a design genuinely requires
|
|
42
|
+
`unserialize()` over data that cannot be fully untrusted-input-free, but do
|
|
43
|
+
not present it to engineers as a fix for untrusted-input reachability — the
|
|
44
|
+
manual's caution stands regardless of the option, and per user-contributed
|
|
45
|
+
notes on the same manual page, `allowed_classes` does not follow class
|
|
46
|
+
inheritance (allowing an interface does not allow its implementers, and
|
|
47
|
+
subclasses of an allowed class do not automatically pass).
|
|
48
|
+
|
|
49
|
+
## The wider magic-method attack surface: `__destruct()` too
|
|
50
|
+
|
|
51
|
+
Object-injection ("PHP Object Injection" / gadget-chain) exploitation is not
|
|
52
|
+
limited to `__wakeup()`/`__unserialize()`. Any class instantiated by the
|
|
53
|
+
`unserialize()` call is a live PHP object with its full lifecycle ahead of
|
|
54
|
+
it, including destruction. Per the php.net manual for destructors: "The
|
|
55
|
+
destructor method will be called as soon as there are no other references to
|
|
56
|
+
a particular object, or in any order during the shutdown sequence." This
|
|
57
|
+
means a reconstructed object with attacker-controlled properties can trigger
|
|
58
|
+
consequential logic in its `__destruct()` even if it has no `__wakeup()` or
|
|
59
|
+
`__unserialize()` at all — including at request shutdown, when no other code
|
|
60
|
+
runs after it. Two destructor-execution details a reviewer should carry
|
|
61
|
+
into any gadget-chain analysis, per the same page:
|
|
62
|
+
`__destruct()` calls made during shutdown happen "in any order" (no
|
|
63
|
+
guaranteed sequence among unrelated objects), and destructors are called
|
|
64
|
+
even when the script stops via `exit()`. When reviewing a class reachable
|
|
65
|
+
from `unserialize()`, check `__wakeup()`, `__unserialize()`, and
|
|
66
|
+
`__destruct()` — all three are automatic, attacker-triggerable entry points
|
|
67
|
+
once the object exists.
|
|
68
|
+
|
|
69
|
+
## RECOMMENDATION: prefer `json_decode()` for untrusted data
|
|
70
|
+
|
|
71
|
+
Match the manual's own stated remediation: for any data interchange that
|
|
72
|
+
crosses a trust boundary (request body, query string, cookie, cache key
|
|
73
|
+
sourced from user input, a queue message an external party can influence),
|
|
74
|
+
use `json_decode()`/`json_encode()` rather than `serialize()`/`unserialize()`.
|
|
75
|
+
If a legacy format genuinely requires PHP's native serialization for
|
|
76
|
+
internal, fully-trusted data only, confirm the value never round-trips
|
|
77
|
+
through anything a client can influence, and consider the manual's mention
|
|
78
|
+
of `hash_hmac()`-based integrity verification for data an application stores
|
|
79
|
+
externally and later reads back, so that tampering can be detected before
|
|
80
|
+
`unserialize()` (or `json_decode()`) is ever called on it.
|
|
81
|
+
|
|
82
|
+
## Reviewer evidence criteria
|
|
83
|
+
|
|
84
|
+
For each `unserialize()` call site:
|
|
85
|
+
|
|
86
|
+
- Identify the exact source of the string passed in. Untrusted sources
|
|
87
|
+
include request parameters, headers, cookies, uploaded file contents,
|
|
88
|
+
values read back from a cache/session/queue backend that a client can
|
|
89
|
+
influence, and any database column a client-facing write path can reach.
|
|
90
|
+
- If the source is untrusted, this is a blocking finding regardless of
|
|
91
|
+
whether `allowed_classes` is set, per the manual's unconditional caution.
|
|
92
|
+
- If `allowed_classes` is present, do not close the finding on that basis;
|
|
93
|
+
note it as partial defense-in-depth only, and still require remediation
|
|
94
|
+
toward `json_decode()` or a fully-trusted data path.
|
|
95
|
+
- For any class the call could instantiate (open allowlist, `false`, or a
|
|
96
|
+
named list), check for `__wakeup()`, `__unserialize()`, and `__destruct()`
|
|
97
|
+
definitions in that class and its parents; if any exists, name it as a
|
|
98
|
+
live attack-surface entry point in the finding, not just a theoretical one.
|
|
99
|
+
- Confirm the recommended remediation is `json_decode()`/`json_encode()`
|
|
100
|
+
(or an equivalent non-object-instantiating format) unless the review
|
|
101
|
+
independently confirms native PHP serialization is required and the data
|
|
102
|
+
never crosses a trust boundary.
|
|
103
|
+
|
|
104
|
+
## Applicable versions
|
|
105
|
+
|
|
106
|
+
The `unserialize()` caution, the automatic `__unserialize()`/`__wakeup()`
|
|
107
|
+
invocation, and the `allowed_classes` behavior described above are current
|
|
108
|
+
php.net manual guidance as of this review; the destructor shutdown-ordering
|
|
109
|
+
and `exit()` behavior is also current manual guidance, with the manual
|
|
110
|
+
separately noting that destructors are not run on shutdown caused by a fatal
|
|
111
|
+
error as of PHP 5.3.10. Re-verify against the live manual pages before citing
|
|
112
|
+
an exact figure for a PHP version outside current support, since documented
|
|
113
|
+
behavior can be refined between manual revisions.
|
|
114
|
+
|
|
115
|
+
## Sources
|
|
116
|
+
|
|
117
|
+
- [PHP Manual — `unserialize()`](https://www.php.net/manual/en/function.unserialize.php) — supports the unconditional caution against passing untrusted input to `unserialize()` regardless of `allowed_classes`, the automatic `__unserialize()`/`__wakeup()` invocation on reconstructed objects, and the `json_decode()`/`json_encode()` remediation recommendation.
|
|
118
|
+
- [PHP Manual — Destructors](https://www.php.net/manual/en/language.oop5.decon.php) — supports the `__destruct()` invocation timing (on last reference removal or in any order during shutdown), invocation even after `exit()`, and the PHP 5.3.10 fatal-error exception to shutdown-time destructor execution.
|
|
119
|
+
- [OWASP Top 10:2021 — A08:2021 Software and Data Integrity Failures](https://owasp.org/Top10/2021/A08_2021-Software_and_Data_Integrity_Failures/index.html) — supports the OWASP category mapping for insecure-deserialization findings (CWE-502: Deserialization of Untrusted Data is listed among this category's notable weaknesses). OWASP has since published a Top 10:2025 edition (https://owasp.org/Top10/2025/) that renumbers this category to A08:2025 – Software or Data Integrity Failures and moves Injection to A05:2025; cite the specific edition and number in any finding rather than assuming 2021 numbering is still current.
|
|
120
|
+
|
|
121
|
+
Last verified: 2026-07-16.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wordpress-rest-block-security-review
|
|
3
|
+
description: Use this skill to review WordPress plugin, theme, REST API, and block-editor code for missing or overly permissive REST permission_callback registration, unescaped dynamic block render_callback output, missing input validation/sanitization or output escaping, and missing nonce/capability checks on state-changing requests. Use when reviewing register_rest_route() calls, dynamic block render_callback/render.php files, form/admin-ajax handlers, or any code path that takes untrusted input and produces WordPress-rendered output. Static review only; it does not execute plugin/theme code and never contacts a live, sandbox, or staging WordPress install.
|
|
4
|
+
allowed-tools: Read Grep Glob
|
|
5
|
+
metadata:
|
|
6
|
+
author: "github: Raishin"
|
|
7
|
+
version: "0.1.0"
|
|
8
|
+
updated: "2026-07-16"
|
|
9
|
+
category: security
|
|
10
|
+
lifecycle: experimental
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# WordPress REST & Block Security Review
|
|
14
|
+
|
|
15
|
+
## Purpose
|
|
16
|
+
|
|
17
|
+
Review the WordPress-specific security seams that a generic PHP reviewer is likely to miss: REST routes registered without an explicit `permission_callback`, dynamic blocks whose `render_callback` emits data without escaping, input that is neither validated nor sanitized before use, and state-changing requests with no nonce or capability check. Plugins and themes are where the WordPress ecosystem's own advisory sources place the large majority of disclosed vulnerabilities, and each of these four gaps is a documented anti-pattern against WordPress's own core APIs, not a stylistic preference.
|
|
18
|
+
|
|
19
|
+
## When to use
|
|
20
|
+
|
|
21
|
+
Use this skill when the user asks to:
|
|
22
|
+
|
|
23
|
+
- review a `register_rest_route()` call or a REST controller class for a missing or overly permissive `permission_callback`,
|
|
24
|
+
- review a dynamic block's `render_callback` or `render.php` for unescaped output,
|
|
25
|
+
- review input handling (REST params, `$_POST`/`$_GET`, shortcode or block attributes) for validation/sanitization gaps, or output for missing escaping,
|
|
26
|
+
- review a form submission, admin-ajax action, or REST write for missing nonce or capability checks.
|
|
27
|
+
|
|
28
|
+
## When not to use
|
|
29
|
+
|
|
30
|
+
Do not use this skill for:
|
|
31
|
+
|
|
32
|
+
- generic PHP defects with no WordPress-specific dimension (raw SQL concatenation with no WordPress DB API involved, generic type-juggling, unrelated logic bugs) — use a general PHP security review for those.
|
|
33
|
+
- infrastructure or hosting-environment hardening (server configuration, TLS, hosting-provider WAF rules); this skill reviews source, not deployment environment.
|
|
34
|
+
- dependency/supply-chain review (vulnerable third-party libraries pulled in via Composer); hand that to a supply-chain reviewer.
|
|
35
|
+
- any live exercise of a WordPress install — executing plugin/theme code, submitting forms, calling REST endpoints, or triggering admin-ajax actions. This skill is static review only.
|
|
36
|
+
|
|
37
|
+
## Preconditions
|
|
38
|
+
|
|
39
|
+
- The plugin or theme source in scope: REST route registrations, block registration and `render_callback`/`render.php` files, form handlers, admin-ajax actions, and shortcode callbacks.
|
|
40
|
+
- The WordPress version the code targets, where relevant to a version-gated API such as `permission_callback` enforcement.
|
|
41
|
+
- Whether any REST route or block output is genuinely intended to be public, so a correctly-scoped `__return_true` or unauthenticated render is not misflagged.
|
|
42
|
+
|
|
43
|
+
## Lean operating rules
|
|
44
|
+
|
|
45
|
+
- Confirm `permission_callback` is present on every `register_rest_route()` call in scope, and that `__return_true` (or an equivalent always-true callback) is used only where the route is genuinely public.
|
|
46
|
+
- Trace every dynamic value in a block's `render_callback`/`render.php` from its source to its output point and confirm the matching escaping function is applied there, not merely present somewhere in the file.
|
|
47
|
+
- Require validation or sanitization of every untrusted input before use, favoring validation/rejection over sanitization alone where a specific check is possible.
|
|
48
|
+
- On non-REST state-changing handlers (form POSTs, `admin-post`/`admin-ajax` actions), require both an explicit nonce verification (`check_admin_referer`/`wp_verify_nonce`) and a `current_user_can()` capability check; neither alone is sufficient.
|
|
49
|
+
- For `register_rest_route()` endpoints, do not flag the absence of an in-handler nonce call: under cookie authentication the REST infrastructure verifies the `wp_rest` nonce automatically before the route runs, so the requirement there is a capability-checking `permission_callback`. A missing/invalid nonce silently demotes the request to anonymous, so it is a finding only when the `permission_callback` assumes an authenticated user.
|
|
50
|
+
- Never request, echo, store, or reproduce a secret, API key, credential, or credential-shaped string found in code; redact-and-flag it instead.
|
|
51
|
+
- Label every claim `repo evidence`, `documentation-based`, or `inference`.
|
|
52
|
+
|
|
53
|
+
## Context7 documentation protocol
|
|
54
|
+
|
|
55
|
+
WordPress REST route requirements, nonce/capability semantics, and block-rendering behavior are documented on developer.wordpress.org and are the only acceptable ground truth for a version or API-behavior claim. Before asserting how `register_rest_route()`, `render_callback`, nonces, or escaping/sanitizing functions behave, cite the current developer.wordpress.org page and label the claim `documentation-based`. If a Context7-indexed source is available for the API in question, prefer it (`resolve-library-id` then `query-docs`) and label the result `context7-grounded`; otherwise use the official documentation directly. Never rely on memorized API behavior for a version-gated claim.
|
|
56
|
+
|
|
57
|
+
## Workflow
|
|
58
|
+
|
|
59
|
+
1. Enumerate the REST routes, dynamic blocks, and state-changing handlers in scope from the files provided.
|
|
60
|
+
2. For each `register_rest_route()` call: confirm `permission_callback` is present; if it is `__return_true` or absent, confirm public-data justification or flag it. See [REST API permission_callback enforcement](references/rest-api-permission-callback.md).
|
|
61
|
+
3. For each dynamic block `render_callback`/`render.php`: trace every dynamic value to its output point and confirm escaping is applied there. See [Dynamic block output escaping](references/dynamic-block-output-escaping.md).
|
|
62
|
+
4. For each input source and output point in scope: confirm validation/sanitization on input and escaping on output, and confirm nonce plus capability checks on state-changing requests. See [Input validation, sanitization, and output escaping](references/input-sanitize-output-escape.md).
|
|
63
|
+
5. Emit findings with evidence tiers, concrete exploit narratives, remediation, and verification steps; hand off any non-WordPress-specific finding to the owning reviewer.
|
|
64
|
+
|
|
65
|
+
## Decision gates
|
|
66
|
+
|
|
67
|
+
- Block only on a finding with a demonstrated reachable path (unauthenticated REST access, unescaped render output, unchecked state change) — not on the mere absence of a keyword.
|
|
68
|
+
- Every WordPress-API version or behavior claim is `documentation-based` (or `context7-grounded` where available), never memory.
|
|
69
|
+
- Every finding names the specific route, block, or handler, not a general area of the codebase.
|
|
70
|
+
- Non-WordPress-specific findings are handed off, not adjudicated here.
|
|
71
|
+
|
|
72
|
+
## Evidence classification
|
|
73
|
+
|
|
74
|
+
Label each finding `repo evidence` (seen directly in the code), `context7-grounded` (current documentation via Context7), `documentation-based` (official developer.wordpress.org documentation fetched directly), or `inference`. Documentation describes the intended pattern; it does not prove what a specific file actually does — always tie the claim back to the file and code path observed.
|
|
75
|
+
|
|
76
|
+
## Security and privacy constraints
|
|
77
|
+
|
|
78
|
+
Static review only. Never execute plugin/theme code, submit forms, call REST endpoints, or trigger admin-ajax actions against any live, sandbox, or staging WordPress install. Never request, echo, store, or reproduce a secret, API key, database credential, or credential-shaped string found in code; treat any such string as a redact-and-flag finding.
|
|
79
|
+
|
|
80
|
+
## Escalation conditions
|
|
81
|
+
|
|
82
|
+
Escalate to incident response any evidence the gap is already reachable in a live, publicly deployed site rather than merely present in source under review. Hand off non-WordPress-specific findings (generic PHP, infrastructure, supply-chain) to the reviewer who owns that area instead of adjudicating them here.
|
|
83
|
+
|
|
84
|
+
## References
|
|
85
|
+
|
|
86
|
+
Load these only when needed:
|
|
87
|
+
|
|
88
|
+
- [REST API permission_callback enforcement](references/rest-api-permission-callback.md) — the required `permission_callback` argument, the `__return_true` public-endpoint pattern, and REST nonce/capability enforcement.
|
|
89
|
+
- [Dynamic block output escaping](references/dynamic-block-output-escaping.md) — how `render_callback`/`render.php` execute and what escaping is required at each output point.
|
|
90
|
+
- [Input validation, sanitization, and output escaping](references/input-sanitize-output-escape.md) — the validate-input/escape-output discipline, sanitizing and escaping function reference, and nonce/capability checks for state-changing requests.
|
|
91
|
+
|
|
92
|
+
## Response minimum
|
|
93
|
+
|
|
94
|
+
Return, at minimum:
|
|
95
|
+
|
|
96
|
+
- the route(s), block(s), or handler(s) in scope and, per finding, the failure class and evidence tier;
|
|
97
|
+
- the concrete exploit narrative (how the gap is actually reachable, not just theoretically present);
|
|
98
|
+
- concrete remediation naming the specific WordPress function or pattern to add, and an exact verification step;
|
|
99
|
+
- handoffs for any non-WordPress-specific finding, and any incident-response escalation.
|
|
100
|
+
|
|
101
|
+
## Anti-goals
|
|
102
|
+
|
|
103
|
+
- Do not expand into generic PHP review; own the WordPress-specific seam, hand off everything else.
|
|
104
|
+
- Do not execute plugin/theme code or contact any live, sandbox, or staging WordPress install.
|
|
105
|
+
- Do not echo, reproduce, or transmit any secret, credential, or credential-shaped string.
|
|
106
|
+
- Do not assert a WordPress API version or behavior from memory.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "wordpress-rest-block-security-review",
|
|
3
|
+
"name": "WordPress REST & Block Security Review",
|
|
4
|
+
"type": "skill",
|
|
5
|
+
"provider": "php",
|
|
6
|
+
"harnesses": ["claude-code", "cursor", "codex", "gemini", "kiro", "other"],
|
|
7
|
+
"summary": "Skill for reviewing WordPress plugin and theme security: REST register_rest_route permission_callback enforcement (required since WordPress 5.5) and nonce/capability checks, dynamic-block render_callback output escaping, and the validate-on-input, escape-on-output discipline behind the plugin-dominated WordPress CVE surface.",
|
|
8
|
+
"source_type": "original",
|
|
9
|
+
"official_docs": [
|
|
10
|
+
"https://developer.wordpress.org/apis/security/",
|
|
11
|
+
"https://developer.wordpress.org/rest-api/extending-the-rest-api/routes-and-endpoints/",
|
|
12
|
+
"https://developer.wordpress.org/block-editor/getting-started/fundamentals/static-dynamic-rendering/",
|
|
13
|
+
"https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/"
|
|
14
|
+
],
|
|
15
|
+
"security_notes": "Static-review-only skill: Read/Grep/Glob, no execution and no live WordPress access. Flags missing permission_callback, unescaped output, and missing nonce/capability checks from source only; treats any credential- or PII-shaped string as a redact-and-flag finding; grounds REST and block-editor API claims in current developer.wordpress.org documentation.",
|
|
16
|
+
"last_verified": "2026-07-16",
|
|
17
|
+
"path": "skills/php/wordpress-rest-block-security-review",
|
|
18
|
+
"author": "github: Raishin",
|
|
19
|
+
"version": "0.1.0"
|
|
20
|
+
}
|