CVE case study
CVE-2026-16532: Link Library's Public Submission Form Reached Raw SQL
Link Library concatenated a URL from its public submission form into a duplicate-check query, creating unauthenticated blind SQL injection before 7.9.3.
- Severity
- High (8.6)
- Scoring
- CVSS 3.0
- Weakness
- CWE-89
- Affected
- Link Library before 7.9.3; local runtime validation covered 7.9.2
- Remediation state
- Upgrade to Link Library 7.9.3 or later
- Advisory published
- 23 Jul 2026
Official vectorCVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N
Why it matters
Link Library lets a site owner place a front-end submission form on a WordPress page with the [link-library-addlink] shortcode. When that form is present, anonymous submission is allowed by the plugin's default settings unless the site owner explicitly requires a login.
The form's link_url value reached a duplicate-link SELECT query after sanitize_url(). URL sanitization is not SQL parameterization. In 7.9.2, the sanitized value was still placed between quotes in a query string and sent to $wpdb->get_var().
This produced a blind SQL injection primitive. My local validation used boolean behavior and a timing condition only. I did not extract WordPress data or test a third-party site. WPScan rates the verified issue High at 8.6 and lists confidentiality as the confirmed vulnerable-system impact.
The WordPress plugin directory reported 10,000 or more active installations when reviewed on 23 July 2026. That is reach context, not a claim that every installation was vulnerable or exposed. A site still needed an affected version and a page containing the front-end submission form.
How I found it
I found this during a fresh plugin-update diff review and independently validated the issue against Link Library 7.9.2. Version 7.9.3 had just been tagged with the short changelog entry Fixed potential security issue
, so I compared the old and new source to identify the security-relevant change.
The diff isolated a duplicate-link query in usersubmission.php. Version 7.9.2 concatenated the submitted link name, URL, and optional domain value into SQL. Version 7.9.3 replaced those values with placeholders and passed them through $wpdb->prepare().
I then traced the old query backward. The [link-library-addlink] shortcode renders a front-end form, includes the nonce and dispatcher field, and accepts link_url. The plugin's defaults allow anonymous submission and disable CAPTCHA unless the site owner changes those settings. The request dispatcher loads the submission handler before the URL reaches the duplicate check.
The key mistake was treating sanitize_url() as protection for SQL. WordPress request slashing can add a backslash before a quote, while the URL sanitizer's database-context character filtering can remove that backslash and retain the quote. A URL-safe value was therefore still able to alter SQL syntax when concatenated into the query.
The public 7.9.3 tag was created at 04:17:42 +0300 on 21 July 2026. My initial source evidence was recorded at 04:42:48, followed by complete anonymous HTTP validation from 09:36 to 09:40. I submitted the independent validation to WPScan that day.
The local WordPress lab confirmed a boolean result difference, a controlled 3.12-second delay on 7.9.2, and a 0.04-second patched control on 7.9.3. I used no production target and extracted no data. WPScan verified the report and published CVE-2026-16532 with researcher credit on 23 July 2026.
Root cause
The handler read $_POST['link_url'] and passed it through sanitize_url(). WordPress adds slashes to request data, but the URL sanitizer calls the database-context URL escaping path. Its character filter can remove an added backslash while leaving a single quote. The value is acceptable as a URL string but is not safe to concatenate into SQL.
The vulnerable duplicate check built its query by placing $link_url, $link_name, and an optional domain comparison directly into SQL text. The final string was executed through $wpdb->get_var() with no prepared placeholder at the URL boundary.
Version 7.9.3 fixes the boundary rather than adding another input filter. It converts the dynamic values to %s placeholders, collects the values separately, and executes the query through $wpdb->prepare() before calling get_var().
Source-to-sink trace
- 01Public form
render-link-library-addlink-sc.php:323-391The add-link shortcode renders a front-end POST form, a public LL_ADDLINK_FORM nonce, the submission dispatcher flag, and the link_url input.
- 02Default reachability
link-library-defaults.phpThe default settings do not require login and leave CAPTCHA disabled. A site must expose the shortcode page, but anonymous submission is otherwise the default path.
- 03Request dispatch
link-library.php:2287-2290The template_redirect handler sees link_library_user_link_submission, loads usersubmission.php, and calls link_library_process_user_submission().
- 04Attacker-controlled value
usersubmission.php:52 in 7.9.2The handler reads POST link_url and applies sanitize_url(). That function validates URL content but does not parameterize a later SQL query.
- 05Raw SQL construction
usersubmission.php:259-275 in 7.9.2The sanitized URL is inserted between quotes in the duplicate-link SELECT string, which is executed by $wpdb->get_var().
- 06Prepared-query fix
usersubmission.php in 7.9.3The patch replaces each dynamic SQL value with a %s placeholder and passes the value list through $wpdb->prepare() before execution.
Safe proof of concept
Prerequisites
- A local WordPress test installation that you own, with Link Library 7.9.2 available for the affected check and 7.9.3 available for the fixed control.
- A page containing the Link Library front-end add-link shortcode and a test library category.
- Command-line tools for offline source comparison. The source-diff step needs only curl and diff.
- No real user data. Populate the lab with synthetic links and record only response behavior and timing.
Step-by-step reproduction
- Download the public 7.9.2 and 7.9.3 submission handlers and compare them. Confirm that the old duplicate check concatenates dynamic values and the new code uses placeholders with $wpdb->prepare().
- In the local 7.9.2 lab, open the page containing the front-end add-link form. Confirm that the page itself renders the nonce and submission fields needed for an ordinary anonymous submission.
- Send a normal synthetic link first and record the baseline response, database result, and elapsed time.
- Repeat the request with a redacted controlled-true SQL condition inside link_url. Record whether the duplicate check follows the true branch.
- Repeat with the paired controlled-false condition. A repeatable difference between the two conditions establishes a boolean oracle without reading actual database content.
- Run one redacted timing condition in the same local lab. The recorded 7.9.2 request took 3.12 seconds while ordinary requests completed between 0.03 and 0.12 seconds.
- Install 7.9.3 and send the same timing control. The request completed in 0.04 seconds and the value remained data inside the prepared query.
- Keep the operational condition private until WPScan's 6 August 2026 PoC date. The placeholders below document the method without publishing an executable payload.
Compare the public vulnerable and fixed handlers
LAB_DIR=$(mktemp -d "\${TMPDIR:-/tmp}/link-library-cve.XXXXXX")
curl -fsSL \
https://plugins.svn.wordpress.org/link-library/tags/7.9.2/usersubmission.php \
-o "$LAB_DIR/usersubmission-7.9.2.php"
curl -fsSL \
https://plugins.svn.wordpress.org/link-library/tags/7.9.3/usersubmission.php \
-o "$LAB_DIR/usersubmission-7.9.3.php"
diff -u \
"$LAB_DIR/usersubmission-7.9.2.php" \
"$LAB_DIR/usersubmission-7.9.3.php"Redacted local request shape
POST /public-add-link-page/ HTTP/1.1
Host: wordpress.local
Content-Type: application/x-www-form-urlencoded
link_library_user_link_submission=1
&_wpnonce=<NONCE_RENDERED_BY_THE_PUBLIC_FORM>
&settingsid=1
&link_category[]=1
&link_name=synthetic-test
&link_url=https://example.invalid/<QUOTE_BREAK><CONTROLLED_CONDITION><COMMENT_MARKER>The fixed SQL boundary in 7.9.3
// Condensed from the public 7.9.3 patch.
$query = "SELECT COUNT(1)
FROM ...
WHERE p.post_title = %s
OR pm.meta_value = %s";
$params = array( $link_name, $link_url );
$link_name_exists = $wpdb->get_var(
$wpdb->prepare( $query, $params )
);Redacted local timing evidence
7.9.2 normal baseline 0.03 to 0.12 seconds
7.9.2 controlled delay 3.12 seconds
7.9.3 same patched control 0.04 seconds
No production host was contacted.
No database value was extracted.
The operational SQL condition remains withheld until 6 August 2026.Expected evidence
- The source diff shows raw concatenation in 7.9.2 and %s placeholders with $wpdb->prepare() in 7.9.3.
- On 7.9.2, the controlled-true and controlled-false conditions produce repeatably different duplicate-check behavior.
- The redacted timing control delays the 7.9.2 response to about 3.12 seconds while the normal baseline remains below 0.12 seconds in the same local lab.
- On 7.9.3, the same control completes in about 0.04 seconds and does not influence query structure.
- The database log for 7.9.2 shows the submitted URL inside the query text. The 7.9.3 query log shows it handled as a prepared value.
Submit a normal URL and a pair of synthetic condition markers that contain no SQL control characters. They should follow the ordinary link-submission path with no condition-dependent result and no added delay.
Repeat the same redacted boolean and timing controls on Link Library 7.9.3. The URL must remain a single prepared value, response behavior must not depend on the condition placeholder, and elapsed time should return to the local baseline.
Impact
An unauthenticated visitor can reach the vulnerable branch when a site exposes the front-end add-link form and the relevant submission settings permit the request. The nonce rendered in that public form protects request intent; it is not an authentication control.
The confirmed primitive is blind SQL injection in a duplicate-check SELECT. Boolean behavior and a controlled delay showed that attacker-controlled SQL syntax influenced database evaluation. That class of primitive can be used to infer confidential database values, which matches WPScan's High 8.6 record.
The research did not demonstrate direct database writes, deletion, remote code execution, site takeover, or extraction of real user data. The form must also be deployed on the target site. Those limits are part of the finding, not reasons to leave the affected query unpatched.
Fix and retest
Upgrade Link Library to 7.9.3 or later. Sites that cannot update immediately should remove or disable public access to the front-end add-link form, but access restrictions are only a temporary exposure reduction.
Retest with three controls in a local environment: a normal URL, a redacted boolean-condition request against 7.9.2, and the same request against 7.9.3. The patched version should treat the entire URL as data, preserve normal submission behavior, and show no condition-dependent result or delay.
Review other hand-built WordPress queries for the same assumption. Sanitizers for URLs, HTML, filenames, and text solve format-specific problems. SQL values still need $wpdb->prepare() placeholders at every query boundary.
Engineering lesson
A value can be valid for its business format and still be unsafe for the next interpreter. The durable fix belongs at the SQL boundary: keep query structure and request data separate.
Small security changelog entries deserve source-diff review. In this case, comparing 7.9.2 with 7.9.3 exposed the exact query change, and tracing backward from that sink revealed the public form, defaults, nonce, dispatcher, and sanitizer behavior that made the issue reachable.
References
- WPScan verified record and researcher credit opens in a new tab
- Link Library 7.9.2 vulnerable handler opens in a new tab
- Link Library 7.9.3 patched handler opens in a new tab
- Link Library 7.9.2 front-end form renderer opens in a new tab
- WordPress plugin page and 7.9.3 changelog opens in a new tab
- CWE-89: Improper Neutralization of Special Elements used in an SQL Command opens in a new tab