36 KiB
Security, Reliability & PII Audit — esp-instanda-integration
Plugin version: 1.0.7 · Audit date: 2026-07-02 · Standards: Getfused Universal Coding Standards
Target: local WordPress Studio copy of hub-insurance (SQLite, WP 7.0, PHP 8.5, GF 2.10.5)
Method: static review → adversarial verification (one skeptic per finding + completeness critics)
→ live read-only inspection → in-process state-machine tests (INSTANDA mocked; zero external API traffic).
Report-only. No plugin code was changed. The only site writes were one throwaway transaction row and a temporary read-only probe file, both deleted; the transaction table is back to its original 14 rows. Severities below were reconciled against an adversarial verification pass — it refuted 6 candidate findings and downgraded several others (see §5), which is why some items are lower than a first read would suggest.
1. Executive summary
This is a well-built add-on. It has a write-ahead transaction log, a test-mode lock enforced in
three independent layers, redacting logging, Throwable guards on every hook so a plugin bug
can't white-screen the form, and libsodium encryption for the stored API password. Most of that
machinery was verified to work as designed. Several things a first read flags as alarming turned
out to be non-issues on inspection — no public information disclosure, no SQL injection, no
admin-page privilege leak, and the form is not mis-wired into unsubmittability (§5).
What remains are the failure modes that matter for a form which spends money and stores PII on every submission, because the plugin's two guarantees — no lost quotes, no duplicate policies — are not fully met. The load-bearing items, each confirmed with live evidence on the running site:
- Duplicate INSTANDA quotes on repeat submits (F-01) — proven live: one email submitted 10 times produced 10 distinct quotes. Duplicate-prevention only stops a double-fire within one request (and production's object cache doesn't change this — it's token instability).
- Completing the Go-Live flow can leave real quotes going to the Test endpoint (F-03) —
switch_to_live()flips the mode but never sets the environment. - The stored API password is recoverable from a DB dump — confirmed on both the local and
the production database: no
SPG_INSTANDA_KEYconstant anywhere, so the encryption key sits beside the ciphertext it protects (F-04). - Plaintext PII with indefinite retention, and failure alerts silently disabled (no alert recipients) — confirmed in production too (F-05).
- Config drift + a stray active feed on the test form (F-02) — production has Form 5 correctly wired and an active feed still attached to the test Form 6; the local copy has the feed on the wrong form entirely.
Remediation is tractable and prioritized in §7. Production is verified to be pre-launch (still in INSTANDA test mode), so these are fixable before go-live.
2. Scope, environment & method
In scope: the plugin (wp-content/plugins/esp-instanda-integration/, 18 files, ~4,900 lines)
and its front-end coupling in the active theme (wp-content/themes/GFeneratePress/functions.php).
Live environment observed (read-only):
| Fact | Value | Why it matters |
|---|---|---|
| Active theme | GFeneratePress |
carries the Form 5 date validator |
| Timezone | America/New_York (gmt_offset -4) |
makes the TZ-mixing bug a real 4h skew (F-07) |
SPG_INSTANDA_* constants |
none defined | credentials + key resolve to the DB (F-04) |
GRAVITYSMTP_SENDGRID_API_KEY |
defined in wp-config.php |
hardcoded secret (F-08) |
| Object-cache drop-in | absent (wp_using_ext_object_cache()=no) |
cross-request lock is inert locally |
| INSTANDA feed | Form 6 only (event_start=7, event_end=8, customer_email=9) |
F-02 |
| Live Form 5 | dates at 8/9, email at 10; no feed; page "Form Test Page" | F-02 |
| Operating mode / env | unset→test / test; domain consumer.instanda.us |
mode clamp active |
alert_emails |
empty | failure alerts silently no-op (F-05) |
| Retention / storage | retention_days=7, storage_enabled=1 |
delivered-only purge |
| Cleanup cron | esp_instanda_cleanup daily (one instance) |
correct |
| Transaction table | 14 rows (13 delivered, 1 failed), all Form 6, environment=test |
real test data |
Verification passes: (a) full static read of the six load-bearing files + architecture map of
all 18; (b) an adversarial pass — one agent per candidate finding tasked with refuting it, plus
three completeness critics; (c) live probes via wp eval/eval-file (wp db query is unusable
on this SQLite build — it probes MySQL sql_mode); (d) in-process pre_http_request mocks driving
the real Api_Client/Transaction_Store through every response class with no external calls.
Production parity: verified via read-only WP-CLI on the live Pressable hubinsurance site
(§6) — object cache, secrets/constants, sql_mode, schema, feeds, and options.
3. Severity summary
| # | Finding | Severity | Status |
|---|---|---|---|
| F-01 | Duplicate INSTANDA quotes on repeat submits (token instability) | High | Live-proven |
| F-03 | switch_to_live() doesn't set env → live quotes keep hitting Test |
High | Code-confirmed |
| F-04 | API password recoverable from a DB dump (self-provisioned key) | High | Confirmed local + prod |
| F-02 | Stray active feed on test Form 6 + local↔prod feed drift (prod Form 5 wired) | Medium | Live-confirmed |
| F-05 | Plaintext PII, indefinite retention, alerts disabled | Medium | Live-confirmed |
| F-06 | No rate-limit / no CAPTCHA on public → API path | Medium | Live-confirmed |
| F-07 | Timezone mixing skews retention & resubmit self-recovery | Medium | Confirmed (UTC-4) |
| F-08 | Hardcoded SendGrid API key in wp-config.php |
Medium | Live-confirmed |
| F-09 | Async recording fails silently; Form 6 has zero notifications | Medium | Confirmed |
| F-10 | 5xx auto-retry of the mutating PUT is a duplicate vector | Medium | Verified |
| F-11 | Guard::wrap fails open on validation → quote-less entries |
Medium | Confirmed |
| F-12 | Delivered transaction can be resubmitted → duplicate quote | Medium | Code-confirmed |
| F-13 | Stranded-lead delivered row is purged by retention → lost lead | Medium | Confirmed |
| F-14 | Write-ahead insert failure / storage-off → silent StartQuote | Medium | Confirmed |
| F-15 | Uninstall keeps the encrypted password and its key + all options | Medium | Confirmed |
| F-16 | "Gravity Forms required" admin notice is unreachable when GF is absent | Medium | Code-confirmed |
| F-17 | 200-without-quoteRef recorded as failed, not possible-duplicate |
Low | Verified |
| F-18 | Resubmit self-recovery over-matches → skips legitimate resubmits | Low | Confirmed |
| F-19 | DB-tamper trust boundary (resubmit replays stored body; outbound mail) | Low | Confirmed |
| F-20 | Operator-misleading dead code (retry count, resubmit event never fire) | Low | Confirmed |
| F-21 | event_log non-atomic read-modify-write drops concurrent audit events |
Low | Confirmed |
| F-22 | Retention cron scheduled only at activation, never re-ensured | Low | Confirmed |
| F-23 | Front-end hygiene: ~220 lines inline JS sitewide + observer never disconnects | Low | Confirmed |
| F-24 | Uninstall/deactivation option residue; stale docblocks/template | Info | Confirmed |
§5 lists what was checked and cleared (including 6 refuted candidates).
4. Findings
F-01 · Duplicate INSTANDA quotes on repeat submits — High · Live-proven
The duplicate-prevention scheme keys off submission_token()
(class-submission.php:219-229),
which uses GFFormsModel::get_form_unique_id — a fresh uniqid() per request for this
non-upload form (or a client-supplied value). So the per-submission lock, replay cache, and the
"already delivered" short-circuit (:63-71)
only match within a single request. Across a browser re-POST, a back-button resubmit, or the
retry the plugin invites after an ambiguous timeout ("Please try again in a moment",
:312), the
token differs, the dedupe key differs, and a new PUT /StartQuote fires. The dedupe key is
computed but never transmitted, and INSTANDA has no idempotency.
Live proof. 14 rows, 14 distinct dedupe keys, 13 distinct quote refs. zbatte@getfused.com
appears in 10 submissions, each with a different quote ref (rows 12 and 13 are 36 s apart →
CVPAZE and Y38STE). The delivered-dedupe short-circuit has never fired in real use.
Reconciliation. Rated High, not Critical: on the default posture the traffic hits the Test endpoint (mode clamp), so this creates duplicate Test quotes today. Once live (F-03), it creates duplicate real policies.
Fix. Derive the dedupe key from submission content + form id (hash of the mapped field values), so identical resubmits collapse; keep the per-request lock as a second layer; persist the delivered short-circuit by content hash so a re-POST returns the prior quote URL instead of a new quote.
F-02 · Stray active feed on the test form + local↔prod feed drift — Medium · Live-confirmed
The two environments are configured differently, and both carry loose ends:
| Form 5 "Start a Quote" (dates 8/9, email 10) | Form 6 "…(INSTANDA test)" (dates 7/8, email 9) | |
|---|---|---|
| Production | active feed 8/9/10 ✓ + theme validation 8/9 ✓ → correctly wired |
active feed still attached (test form) |
| Local | no feed (theme validates it, but it never reaches INSTANDA) | active feed 7/8/9 — the only fed form; all 14 tx came from it |
So on production, AGENTS.md's "Form 5 is the live form" actually holds — Form 5's feed mapping and
the theme's hardcoded 8/9 agree. The residual issues are: (a) a still-active INSTANDA feed on
the test Form 6 in production — if that page is reachable it also creates real (currently Test)
quotes and gets no theme date validation
(functions.php:128-130 only handles
form id 5); and (b) config drift — the local copy has the feed on Form 6 only, so testing
Form 5 locally never exercises the INSTANDA path that runs in prod. The theme validator being
hardcoded to a single form id (functions.php:74-76)
is the underlying fragility. (The earlier worry that a field mismatch bricks the form was
refuted — §5; every fed form is individually wired correctly.)
Fix. Deactivate the INSTANDA feed on the test Form 6 in production (or retire the form); align
the local copy with prod so tests exercise the live path; drive the theme's GF_DATE_FORM_ID from
the form that actually has the active feed rather than a hardcoded constant, and add a go-live
assertion that the public quote page embeds exactly one fed, theme-validated form.
F-03 · switch_to_live() doesn't set the environment — High · Code-confirmed
The sanctioned go-live action sets the operating mode but not the environment:
// class-go-live.php
switch_to_live() { update_option('espinstanda_operating_mode', 'live'); … } // :131 — no env write
return_to_test() { update_option('espinstanda_operating_mode', 'test');
update_option('espinstanda_env', 'test'); … } // :145-146 — sets BOTH
Config::environment() returns Test whenever raw_env() isn't 'live'
(class-config.php:87-98),
and raw_env() reads the espinstanda_env option (currently test). So an operator who completes
the Go-Live checklist and clicks "switch to live" ends up with operating_mode=live but
environment=Test — the test-mode lock is now off, yet real customer quotes keep going to the
INSTANDA Test endpoint with Test credentials. The asymmetry with return_to_test() (which sets
both) shows this is a bug, not a design choice.
Mitigation / caveat. If the intended Live posture is followed — SPG_INSTANDA_ENV='live' (or
the settings env radio) set as part of go-live — raw_env() returns 'live' via the constant and
the switch works. So this bites the operator who relies on the Go-Live button alone. Given that
button is the sanctioned flow, this is a High-priority correctness bug.
Fix. Have switch_to_live() set espinstanda_env='live' (mirroring return_to_test()), or
make environment() follow operating_mode directly.
F-04 · API password recoverable from a DB dump — High · Confirmed local + production
With no SPG_INSTANDA_KEY constant, the libsodium key is self-provisioned into the DB option
espinstanda_secret_key
(class-config.php:193-226) —
i.e. the key lives in the same database as the ciphertext it protects. Verified on both
environments: neither local nor the production hubinsurance site defines SPG_INSTANDA_KEY
(nor any SPG_INSTANDA_* credential/env constant), and on both the key (len 44 = a 32-byte sodium
key) and the ciphertext (espinstanda_password_test, len 76) are present in the database. A
database dump alone therefore yields the API password on the live site. The class-level docblock
(class-config.php:13)
asserts the opposite as an absolute ("a database dump alone never exposes a usable secret");
only the private docblock at :205-211 admits the caveat.
No mitigation in place. The intended-to-be-hardened path — setting SPG_INSTANDA_KEY (and the
credential constants) in wp-config.php to move secrets out of the DB — is not used in
production, so the guarantee the docblock states is not met anywhere today.
Fix. Set SPG_INSTANDA_KEY + credential constants in prod wp-config.php and make it a
blocking go-live check; correct the class-13 docblock to state the caveat. (This also removes
the F-15 uninstall exposure, since the key would no longer be in the DB.)
F-05 · Plaintext PII, indefinite retention, alerts disabled — Medium · Live-confirmed
redacted_payload is a misnomer: it stores the full StartQuote JSON including the customer email
(redact() strips only auth/password tokens, never PII —
class-logger.php:39-43),
alongside plaintext customer_email/role_label columns
(class-transaction-store.php:104-108).
Retention purges delivered-only
(:314-322),
so failed/pending rows keep PII forever with no data-subject deletion path. On mail-send failure,
the customer email is also logged and fanned to a public action hook
(class-notifier.php:70-78).
Live: real emails are in the table today, and espinstanda_alert_emails is empty, so the
failure-alert path silently no-ops — the one real 400 failure generated no alert.
Fix. Rename the column and document what it holds; add a retention sweep (or email anonymization) for failed/pending rows; add a delete-by-email routine for data requests; require ≥1 alert recipient at go-live.
F-06 · No rate-limit / no CAPTCHA on the public → API path — Medium · Live-confirmed
Every final-page submission of a form with an active feed reaches a live PUT /StartQuote. The
only throttle is the per-submission lock, which doesn't limit distinct scripted submissions (it
only dedupes a same-token double-fire). Production has a persistent object cache (verified), so the
lock is atomic cross-request there; locally it has no object cache and is inert — either way it
doesn't throttle distinct submits. Anti-bot is delegated to GF's is_valid
(class-submission.php:51-53),
and Form 6 has no CAPTCHA field (verified) — only a honeypot, which (per the verification pass)
actually fires on gform_entry_is_spam after the plugin's create runs in gform_validation, so
it doesn't gate the create at all. A PII row is inserted before every call (F-05), and retention
purges delivered-only, so failed/attacker traffic accumulates.
Reconciliation. Rated Medium, not Critical: the test-mode clamp means the flood hits the Test endpoint by default (prod is still in test mode), so the paid-API cost-DoS is gated behind go-live (F-03); Field_Mapper's required-field validation adds friction; edge rate-limiting (Cloudflare/Pressable) may exist but was not confirmed. The mechanism — no application-level rate limit and no CAPTCHA on the live form — is confirmed.
Fix. Add a CAPTCHA (or Akismet) to the live quote form; add server-side rate limiting keyed by
IP and email before create_pending; cap/alert on pending+failed row growth.
F-07 · Timezone mixing skews retention & resubmit self-recovery — Medium · Confirmed (UTC-4)
Rows are written with site-local current_time('mysql')
(class-transaction-store.php:140,162),
but the retention cutoff (:317),
the resubmit self-recovery window (:246-249),
and compute_expiry (:338-342)
use UTC gmdate(). Live: this site is America/New_York; at audit time current_time was 16:58
while gmdate was 20:58 — a real 4-hour divergence. The purge runs ~4h off its boundary, the 24h
self-recovery window is shifted (feeding F-01 or F-18), and delivered_at/expiry are on different
clocks. Fix: use UTC everywhere (gmdate/current_time('mysql', true)) for both writes and
cutoffs.
F-08 · Hardcoded SendGrid API key in wp-config.php — Medium · Live-confirmed
GRAVITYSMTP_SENDGRID_API_KEY is defined in wp-config.php with a live-looking SG.… value next
to the salts (confirmed present; value not extracted). This is the SMTP add-on's key, but it's in
the audited config surface and powers the plugin's customer emails. Fix: move it to an
environment secret; rotate it if this copy has ever been committed, backed up, or shared
(escalates to High in that case).
F-09 · Async recording fails silently; Form 6 has zero notifications — Medium · Confirmed
With _async_feed_processing=true, record_feed_result runs in a later request where
Submission::$current_dedupe is null; if on_entry_created didn't attach the entry
(a Guard-swallowed throw, storage disabled, or a missing dedupe row) it returns with no meta,
no note, no notification, and no log line
(class-instanda-addon.php:369-403).
Live, this is compounded: Form 6 has zero notifications configured, so the
espinstanda_quote_success notification the code sends has nothing to fire — the customer
"quote ready" email is currently inert. Fix: fall back to find_by_entry/stash and log when a
transaction can't be resolved; configure + test the success notification on the canonical form.
F-10 · 5xx auto-retry of the mutating PUT is a duplicate vector — Medium · Verified
request() re-sends the mutating PUT /StartQuote after a 5xx/transport error
(class-api-client.php:121-141),
contradicting the class's own "never auto-retry a mutating ambiguous call" docblock. With no
idempotency and the dedupe key untransmitted, a gateway 5xx after INSTANDA processed attempt 1
creates a second quote. Verified in-process: a persistent 500 makes 2 attempts. Timing
nuance: a genuine slow timeout (8 s) exceeds the 3 s retry budget after attempt 1, so real
timeouts make only 1 attempt — the auto-retry duplicate risk is real for fast 5xx/connection
failures, not slow timeouts. Fix: don't auto-retry a non-idempotent create.
F-11 · Guard::wrap fails open on validation — Medium · Confirmed
Guard::wrap returns $args[0] on any Throwable
(class-guard.php:24-33).
On gform_validation that leaves is_valid true, so a bug produces a saved entry with no quote
and the default confirmation — a silent quote-less lead. The fail-open trade-off ("never break the
form") is defensible, but the validation path specifically should record a pending/failed
transaction and fire the esp_instanda_throwable alert so the lost quote is visible.
F-12 · A delivered transaction can be resubmitted → duplicate quote — Medium · Code-confirmed
Resubmit_Handler::run has no delivered-status guard, and the self-recovery check
$recent->id !== $tx->id deliberately does not skip when the recent success is the row being
resubmitted (class-transactions-table.php:144-149).
So resubmitting an already-delivered transaction (via the admin AJAX endpoint, which also lacks a
status guard, class-instanda-addon.php:582-604)
fires a second PUT /StartQuote and overwrites the original quote_ref in the row — a duplicate
quote plus loss of the first quote's reference. Requires the espinstanda_transactions capability.
Fix: refuse to resubmit rows whose status is already delivered.
F-13 · Stranded-lead delivered row is purged by retention — Medium · Confirmed
On the no-entry success path, if both urlSingleUse and the continue_quote re-mint fail, the
quote is delivered but the customer is never notified and no GF entry exists — only the delivered
transaction row (class-transactions-table.php:162-173).
That row is then purged after the retention window
(class-transaction-store.php:314-322),
so the lead is lost with no trace. Fix: exempt delivered rows with a null entry_id (or an
unsent customer email) from the retention purge, or resolve them before purge.
F-14 · Write-ahead insert failure / storage-off → silent StartQuote — Medium · Confirmed
create_pending returns 0 on insert failure and the caller guards all state marking behind
if ($tx_id) while still making the StartQuote call
(class-submission.php:86-125).
The same guard means that when storage is disabled (an allowed, advisory-only posture) every
StartQuote failure is completely silent — no row, no alert. So the write-ahead log's core promise
(never make an unaudited call) is void exactly when it's needed. Fix: if storage is enabled and
create_pending returns 0, abort or log before proceeding; keep a failure record/alert independent
of the row insert.
F-15 · Uninstall keeps the encrypted password and its key — Medium · Confirmed
uninstall.php deletes only espinstanda_db_version and espinstanda_operating_mode_audit,
leaving ~13 other espinstanda_* options including both espinstanda_password_test (ciphertext)
and espinstanda_secret_key (its key), plus env, site domain, retention, alert emails, and the
go-live acknowledgements (uninstall.php:16-32).
The recoverable secret pair (F-04) survives uninstall, and a reinstall inherits stale go-live state.
Fix: delete all espinstanda_* options and transients on uninstall (behind a "remove data"
setting if retention is ever desired).
F-16 · "Gravity Forms required" notice is unreachable when GF is absent — Medium · Code-confirmed
The requirement notice is registered inside the gform_loaded callback
(esp-instanda-integration.php:86-97),
but gform_loaded only fires when Gravity Forms is present. So if GF is deactivated, the "Gravity
Forms is required and is not active" notice (:69)
never registers — the plugin goes silently inactive with no explanation. Fix: register the
GF-absent notice on admin_notices unconditionally (outside gform_loaded).
F-17 to F-24 · Lower-severity findings
| # | Finding | Where | Fix |
|---|---|---|---|
| F-17 | 200-without-quoteRef recorded as failed/possible_duplicate=0 though a quote may exist (verified: ok=true, quote_ref=null → failure branch) |
class-submission.php:101; class-api-client.php:160-163 | Treat 2xx-no-quoteRef (and empty-string ref) as ambiguous |
| F-18 | Resubmit self-recovery matches any delivered quote for the email in 24h → skips a legitimate second-event resubmit (also shifted by F-07) | class-transactions-table.php:144-149 | Scope the match to the specific transaction/dedupe |
| F-19 | Resubmit re-PUTs the stored body verbatim; stranded-lead email uses the DB-stored recipient — DB-tamper → data injection / arbitrary mail (no host SSRF; requires DB write) | class-transactions-table.php:151-173 | Re-validate the decoded body through Field_Mapper before resubmit |
| F-20 | Dead code misleads operators: increment_retry() has no callers (Retries column always 0); espinstanda_resubmit_success event registered but never dispatched |
class-transaction-store.php:191-195; class-notifier.php:20 | Wire up or remove |
| F-21 | event_log updated via non-atomic read-modify-write of the whole JSON blob → concurrent writers (async feed + admin resubmit) silently drop audit events |
class-transaction-store.php:326-336 | Append via a child rows table or a DB-side JSON op |
| F-22 | Retention cron scheduled only in the activation hook, never re-ensured (unlike the schema, which self-heals on admin_init) → if the event is ever lost, purge stops permanently |
esp-instanda-integration.php:41-43 vs :77-82 | Re-ensure the schedule on admin_init |
| F-23 | Theme prints ~220 lines of inline JS in wp_footer on every page where GFForms exists, and its MutationObserver never disconnect()s where the fields never appear |
functions.php:189-422 | Gate to pages embedding the form; disconnect after N tries |
| F-24 | Uninstall option residue (F-15); stale class-config.php:13 docblock (F-04); bundled form/start-a-quote.json template no longer matches the live forms; stale M5/M9/M10 milestone comments |
various | Align docs with shipped behavior |
5. Checked and cleared (including refuted candidates)
The verification pass actively tried to break these and could not — they are not defects:
- Public information disclosure — REFUTED (95%): the public failure path returns only the
generic
esc_html'd message; INSTANDAresponse_excerpt/error_detailappear only on the capability-gated admin detail page. - Admin submenu capability "mismatch" — REFUTED (90%): WordPress core blocks URL access on the menu capability before the render callback runs, so a user lacking the menu cap gets a core 403, not a leaked page. Both directions fail safe; it's a cosmetic inconsistency, not an authz hole.
- Dead bulk "resubmit" as a CSRF hole — REFUTED:
WP_List_Tableauto-emits the_wpnonce, and there is no handler, so the action is an inert no-op (dead code, not a vulnerability). $_POST['p']unsanitized — correct by design: a password must not besanitize_text_field'd; it is used only to build an outbound Basic-auth probe and is not persisted or logged.- Form "bricked" by theme/plugin field conflict — REFUTED: live Form 5 has dates at 8/9 (email at 10), matching the theme's constants; the plugin feed maps Form 6's 7/8/9 correctly. Neither form is mis-validated. (The real issue is the cross-form decoupling — F-02.)
- Strict-
Y-m-ddate failure — REFUTED: live date fields areymd_dash(=Y-m-d), whichField_Mapperaccepts. (Latent coupling only: changing the field format would break parsing.) - Strict-typed entry-integration callbacks TypeError — REFUTED by test: passing a string
form_iddid not throw, becausestrict_typesis caller-scoped and Gravity Forms calls the filters in coercive mode, so a numeric-string id coerces toint. Columns/merge tags register normally in real requests. - SQL injection / stored XSS — all
$wpdbaccess usesprepare()/insert()/update()with column arrays; admin output of stored data isesc_html/esc_url'd. - Test-mode lock — enforced in three independent layers (env clamp, outbound Live assertion,
go-live gate); a mutating Live call while locked throws. All stored data is
environment=test.
6. Live state-machine results & production-parity gaps
State-machine oracle — observed vs expected (verified in-process, INSTANDA mocked; also cross-checked against the 13 live delivered rows and the real 400 failure row):
| Response | ok | quote_ref | is_ambiguous | attempts | resulting tx row |
|---|---|---|---|---|---|
200 + quoteRef |
true | set | false | 1 | delivered (matches 13 live rows) |
200 no body |
true | null | false | 1 | failed, possible_duplicate=0 (F-17) |
400 + errors |
false | null | false | 1 | failed, deterministic (matches live row id 8) |
500 persistent |
false | null | true | 2 | failed, possible_duplicate=1 |
| transport timeout | false | null | true | 2* | failed, possible_duplicate=1 |
* the instant mock retries; a real 8 s timeout makes 1 attempt (retry budget). create_pending → pending and mark_ambiguous → failed/possible_duplicate=1 confirmed directly.
Production (Pressable) parity — VERIFIED via read-only WP-CLI on the live hubinsurance site
(hubinsurance.mystagingwebsite.com, site 1705596, PHP 8.5, wp_environment_type=production,
currently still in INSTANDA test mode — i.e. pre-launch):
| Question | Production result | Effect on findings |
|---|---|---|
| Object cache present? | yes (object-cache.php drop-in present) |
The per-submission lock is atomic cross-request in prod (it was inert locally). Narrows F-06's lock note to local only; F-01 unaffected (token instability, not lock backend). |
SPG_INSTANDA_KEY / credential / SPG_INSTANDA_ENV constants set? |
none — all undef |
F-04 confirmed for prod — the self-provisioned key (len 44) + ciphertext (len 76) both live in the prod DB. The hardened constant posture is not in use anywhere. |
GRAVITYSMTP_SENDGRID_API_KEY |
defined in prod | F-08 confirmed in prod. |
alert_emails |
empty | F-05 confirmed in prod (failure alerts disabled). |
| Operating mode / env | mode=UNSET→test, env=test |
Prod not yet live; mode clamp active. Interacts with F-03 at go-live. |
MySQL sql_mode |
ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER (non-strict, no NO_ZERO_DATE) |
Defuses the schema concern — the DEFAULT '0000-00-00 00:00:00' column built cleanly; wp_esp_instanda_transactions exists on prod. |
| INSTANDA feeds | two active feeds: Form 5 (start=8,end=9,email=10) and Form 6 (start=7,end=8,email=9) |
Reframes F-02 — see below. |
F-02 in production is different from local. On prod, Form 5 is correctly wired: it has its
own active feed whose mapping (8/9/10) matches both its field layout and the theme's hardcoded
8/9, so AGENTS.md's "Form 5 is the live form" holds there. But prod also carries an active feed
on the test Form 6, and the local copy has the feed only on Form 6 (not Form 5). So the real
prod issue is a stray active feed on the test form + local↔prod configuration drift, not lost
quotes — hence F-02 is Medium, not the local-only "wrong form" hazard.
7. Prioritized remediation roadmap
Go-live blockers:
- F-03 —
switch_to_live()must setenv=live(or driveenvironment()from the mode); otherwise the go-live button leaves real quotes hitting Test. - F-01 — content-based dedupe key so repeat submits collapse instead of duplicating quotes.
- F-04 — set
SPG_INSTANDA_KEY+ credential constants in prod (verified absent today); blocking check. - F-05 — PII retention/deletion for failed rows; configure alert recipients (empty in prod).
- F-02 — deactivate the stray INSTANDA feed on the test Form 6 in prod; align local↔prod; add the "one fed, theme-validated public form" go-live assertion.
Soon (data-integrity & correctness):
6. F-06 — CAPTCHA/Akismet + outbound rate limiting on the public create.
7. F-07 — UTC everywhere for timestamps and cutoffs.
8. F-09 / F-11 / F-14 — close the silent-failure paths (async recording, validation throwable, insert/storage-off).
9. F-10 / F-12 / F-17 — stop auto-retrying the non-idempotent create; block delivered-row resubmit; flag 2xx-no-quoteRef as ambiguous.
10. F-08 — move/rotate the SendGrid key out of committed config.
Hygiene (low risk, do when touching the files): 11. F-13, F-15, F-16, F-18–F-24.
8. Appendix — method & limitations
- Static: full read of
class-submission.php,class-config.php,class-api-client.php,class-transaction-store.php,class-go-live.php,class-entry-integration.php,esp-instanda-integration.php, and the themefunctions.php; architecture-mapped all 18 files. - Adversarial verification: one refutation-tasked agent per candidate finding + three completeness critics; verdicts (CONFIRMED / PLAUSIBLE / REFUTED with 0-100 confidence) drove the severities here. Six candidates were refuted (§5); several were downgraded.
- Live (read-only):
wp eval/eval-filefor theme, forms 5/6, feed mapping, options, cron, transaction rows, filter order, object-cache state.wp db queryis unusable on this SQLite build. - Dynamic: in-process
pre_http_requestmock drove the realApi_Client/Transaction_Storethrough success/200-no-body/400/500/timeout with no external calls; one throwaway row was created and deleted. - Production parity: read-only WP-CLI on the live Pressable
hubinsurancesite via the Pressable MCP — object cache,SPG_INSTANDA_*constants, options,sql_mode,SHOW CREATE TABLE, and the INSTANDA feeds (§6). No writes were made to production. - Limitations: no end-to-end browser pass was run, but the redirect/entry flow is evidenced by the 13 live delivered rows; abuse (F-06) and duplicate (F-01) behavior were confirmed from existing data rather than by generating new load against the real API.