CVE case study
CVE-2026-66690: Donation Phone Data Reached GiveWP's Admin View as Stored XSS
GiveWP stored a phone value from a public donation flow and rendered it without output encoding in the privileged donation-detail view. Version 4.16.5.1 escapes that value at the HTML sink.
- Severity
- High (7.1)
- Scoring
- CVSS 3.1
- Weakness
- CWE-79
- Affected
- GiveWP 4.16.5 and earlier
- Remediation state
- Upgrade to GiveWP 4.16.5.1 or later
- Advisory published
- 31 Jul 2026
Official vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L
Why it matters
A public GiveWP donation form can collect a donor phone number without requiring an account. In the affected versions, that value was persisted and later displayed to administrators and GiveWP staff reviewing an individual donation.
The donor-detail view escaped the phone value after an earlier repair, but the separate donation-detail template still printed the same model values into HTML without context-safe output encoding. That left a stored script path in the privileged WordPress administration origin.
The public record scores the issue High at 7.1 and requires a privileged user to open the affected record. The attacker controls storage; execution depends on that later administrative view.
How I found it
On 22 July 2026, I compared GiveWP's recent donor-output repair with every other administrative renderer of the same phone field. The donor view had gained output escaping, while the donation-detail template still emitted the model value directly.
I followed the value backward from the template through Donation::find() and Donor::find() to the public donation form. Field validation applied business rules but did not convert the stored value into safe HTML.
In a disposable local installation, I stored an observable marker through a synthetic donation and confirmed that the affected donation-detail view interpreted it as markup. The same record remained inert after the 4.16.5.1 output-encoding patch.
The final repair covered the donation phone, donor phone, email, and adjacent company output. That broader diff confirmed that the defect was a family of missing HTML sink encoders, not a storage-format issue.
Root cause
Donation::find($payment_id)->phone and Donor::find($donor_id)->phone returned stored form values. The affected template then echoed one value directly or inserted both into sprintf() without esc_html().
The phone field's validators enforced field rules but did not convert arbitrary text into safe HTML. The missing control was output encoding at the final rendering context.
The defect survived an earlier same-class repair because two administrative views rendered the same underlying data through different templates. One sink was fixed while the sibling sink remained raw.
Source-to-sink trace
- 01Public input
GiveWP donation form phone fieldA visitor can supply the donor phone value on a public form when that field is enabled.
- 02Persistent source
Donation and Donor model phone propertiesThe form value is stored for later transaction and donor review; validation does not encode it for HTML.
- 03Privileged renderer
includes/admin/payments/view-payment-details.php in 4.16.5The template selected one or both phone values and printed them directly or through sprintf without esc_html().
- 04Fixed boundary
view-payment-details.php in 4.16.5.1Every phone and email value in the affected branches is encoded with esc_html() immediately before HTML output.
Safe proof of concept
Prerequisites
- Public GiveWP 4.16.5 and 4.16.5.1 source for comparison.
- Python 3 for the inert output-encoding model.
- Optional owned WordPress lab with a test donation form and no payment processing.
Step-by-step reproduction
- Compare the public donation-detail template between 4.16.5 and 4.16.5.1. Locate each added
esc_html()call around phone and email output. - Run the local model with the inert marker
<strong data-cve-marker='safe'>SAFE_MARKER</strong>. It contains no script and performs no action. - Confirm that the affected model returns the marker as markup-shaped text, while the fixed model encodes the angle brackets and quotes.
- If using an owned WordPress lab, insert only the same inert marker into a synthetic donation fixture and inspect the DOM. Do not use JavaScript or any network callback.
- Repeat after upgrading to 4.16.5.1 and confirm that the page displays the literal marker text without creating a
strongelement.
Compare the public donation-detail renderer
LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/givewp-cve.XXXXXX")
curl -fsSL \
https://plugins.svn.wordpress.org/give/tags/4.16.5/includes/admin/payments/view-payment-details.php \
-o "$LAB_DIR/affected.php"
curl -fsSL \
https://plugins.svn.wordpress.org/give/tags/4.16.5.1/includes/admin/payments/view-payment-details.php \
-o "$LAB_DIR/fixed.php"
diff -u "$LAB_DIR/affected.php" "$LAB_DIR/fixed.php"Inert output-encoding model
from html import escape
stored_phone = "<strong data-cve-marker='safe'>SAFE_MARKER</strong>"
def affected(value):
return f"<p>Donor phone: {value}</p>"
def fixed(value):
return f"<p>Donor phone: {escape(value)}</p>"
print("affected:", affected(stored_phone))
print("fixed: ", fixed(stored_phone))Expected evidence
- The source diff adds
esc_html()to both donation and donor phone output branches. - The affected model contains a nested
strongelement, while the fixed model contains only encoded text. - A patched browser DOM must contain the literal
SAFE_MARKERtext but no element carryingdata-cve-marker.
Use an ordinary synthetic phone value such as +1 555 0100. Both models should display the same visible text, demonstrating that contextual encoding preserves normal output.
On GiveWP 4.16.5.1 or later, inspect donor lists, donor overview, and donation details with inert tag-shaped data. Each surface must encode the value as text and preserve the ordinary-phone control.
Impact
An unauthenticated visitor could store active markup through a donation form that collected a phone number. When an authorized staff member opened the donation-detail page, the browser interpreted the stored value in the site's administrative origin.
Code running in that origin can act with the viewing user's browser privileges. The official vector limits each confidentiality, integrity, and availability impact to Low, so this study does not claim that every execution produced full site takeover.
Fix and retest
Upgrade to GiveWP 4.16.5.1 or later. The repair applies esc_html() to the donation and donor phone values in both branches of the donation-detail template. It also escapes adjacent email and company output that crossed the same rendering boundary.
Keep validation for business rules and output encoding for display contexts as separate controls. Stored profile or transaction data must be encoded each time it enters HTML, even if another page already renders the same field safely.
Retest with inert markup in an owned donation form. The affected version should preserve the marker as HTML in the page source; the fixed version must display the marker as text with no new element or script execution.
Engineering lesson
A stored-XSS repair is incomplete until every renderer of the affected model property is inventoried. Search by data accessor and model field, not only by the template named in the first report.
Escape at the sink. A phone validator answers whether the value is acceptable business data; it does not make the value safe for an HTML text node.