Potential unprivileged Stored XSS through wp_targeted_link_rel
High
W
WordPress
Submitted None
Actions:
Reported by
simonscannell
Vulnerability Details
Technical details and impact analysis
The user description is vulnerable to a Stored XSS via an attribute injection. At fault is the `wp_targeted_link_rel()` filter that parses attributes regardless of their position.
```
function wp_targeted_link_rel( $text ) {
// Don't run (more expensive) regex if no links with targets.
if ( stripos( $text, 'target' ) !== false && stripos( $text, '<a ' ) !== false ) {
$text = preg_replace_callback( '|<a\s([^>]*target\s*=[^>]*)>|i', 'wp_targeted_link_rel_callback', $text );
}
```
It essentially just parses the attribute string of all `<a>` tags and passes them to the preg replace callback.
```
function wp_targeted_link_rel_callback( $matches ) {
$link_html = $matches[1];
$rel_match = array();
...
// Value with delimiters, spaces around are optional.
$attr_regex = '|rel\s*=\s*?(\\\\{0,1}["\'])(.*?)\\1|i';
preg_match( $attr_regex, $link_html, $rel_match );
if ( empty( $rel_match[0] ) ) {
// No delimiters, try with a single value and spaces, because `rel = va"lue` is totally fine...
$attr_regex = '|rel\s*=(\s*)([^\s]*)|i';
preg_match( $attr_regex, $link_html, $rel_match );
}
```
As can be seen it then uses a regex to parse the `rel` attribute, its value and its delimeter from the string.
If the rel attribute is found, the following happens:
```
if ( ! empty( $rel_match[0] ) ) {
$parts = preg_split( '|\s+|', strtolower( $rel_match[2] ) );
$parts = array_map( 'esc_attr', $parts );
$needed = explode( ' ', $rel );
$parts = array_unique( array_merge( $parts, $needed ) );
$delimiter = trim( $rel_match[1] ) ? $rel_match[1] : '"';
$rel = 'rel=' . $delimiter . trim( implode( ' ', $parts ) ) . $delimiter;
$link_html = str_replace( $rel_match[0], $rel, $link_html );
```
As you can see the value of the `rel` attribute is splitted by whitespaces and each part is then escaped. The targeted `rel` value is then added to the alread existing ones and put back together.
Most importantly, are the following line:
```
$delimiter = trim( $rel_match[1] ) ? $rel_match[1] : '"';
$rel = 'rel=' . $delimiter . trim( implode( ' ', $parts ) ) . $delimiter;
$link_html = str_replace( $rel_match[0], $rel, $link_html );
```
if the delimeter is empty (e.g. when `rel=abc` has no quotes), the delimer becomes `"`. The original rel attribute is then replaced with the new one.
This is a problem since the following payload:
`<a title=" target='xyz' rel=abc ">PoC</a>`
would turn into
`<a title=" target='xyz' rel="abc" ">PoC</a>` Note that an additional `"` has been injected and the title attribute has been escaped.
This is because the regex to match the rel attribute ignores the position of the `rel` attribute within the attribute string. The above payload shows how the rel attribute is placed within a double quoted attribute. Since no delimeter is set, the delimer becomes a double quote and when the rel attribute is inserted back into the string, the double quote is injected.
I recommend using something like `parse_shortcode_atts()` as in `wp_rel_nofollow()` to prevent this from happening.
By abusing the attribute injection, it is easily possible to create a Stored XSS payload.
Tge `wp_targeted_link_rel()` filter is not only called on the user description, however, this is where it becomes exploitable. This is because this vulnerable filter is added before the `kses` filters are added, which means that the injected attribute would be caught by `wp_post_kses()`. The user description is the only exception where the kses filters are called before `wp_targeted_link_rel()` is called.
`<a href="#" title=" target='abc' rel= onmouseover=alert(/XSS/) ">This is a PoC for a Stored XSS</a>`
## Proof of Concept
The following will demonstrate how a normal forum user can achieve stored XSS on their profile page in BuddyPress
████████
1. This works if the Bio of forum users is displayed in their profile page. Log in as an administrator and go to Appearence -> Customize and then BuddyPress Nouveu -> Member front page and make sure that displaying the user bio is enabled
2. Create a normal forum user account
3. Login and edit your profile. Paste
`<a href="#" title=" target='abc' rel= onmouseover=alert(/XSS/) ">This is a PoC for a Stored XSS</a>` as your user description
4. visit your profil and hover over the link.
## Impact
The Impact of this can vary from site to site. I have shown how this can be exploited in BuddyPress as a mere, normal forum user. Since you can also inject a style attribute and make the link span over the entire page, one can turn this into a wormable Stored XSS in BuddyPress.
Basically every plugin or forum is affected that displays the user description.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Submitted
Weakness
Cross-site Scripting (XSS) - Stored