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

  1. 01
    Public inputGiveWP donation form phone field

    A visitor can supply the donor phone value on a public form when that field is enabled.

  2. 02
    Persistent sourceDonation and Donor model phone properties

    The form value is stored for later transaction and donor review; validation does not encode it for HTML.

  3. 03
    Privileged rendererincludes/admin/payments/view-payment-details.php in 4.16.5

    The template selected one or both phone values and printed them directly or through sprintf without esc_html().

  4. 04
    Fixed boundaryview-payment-details.php in 4.16.5.1

    Every 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

  1. 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.
  2. Run the local model with the inert marker <strong data-cve-marker='safe'>SAFE_MARKER</strong>. It contains no script and performs no action.
  3. Confirm that the affected model returns the marker as markup-shaped text, while the fixed model encodes the angle brackets and quotes.
  4. 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.
  5. Repeat after upgrading to 4.16.5.1 and confirm that the page displays the literal marker text without creating a strong element.

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 strong element, while the fixed model contains only encoded text.
  • A patched browser DOM must contain the literal SAFE_MARKER text but no element carrying data-cve-marker.
Negative control

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.

Fixed-version re-test

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.

References

Follow this thread

Related research, source, and writing.

These paths share a published research boundary, project source, article series, or authorized lab context with this record.

Back to article start