Skip to content

RTC: Use prepared queries instead of *_post_meta functions#11325

Open
chriszarate wants to merge 2 commits intoWordPress:trunkfrom
chriszarate:fix/sync-post-meta-prepared-queries
Open

RTC: Use prepared queries instead of *_post_meta functions#11325
chriszarate wants to merge 2 commits intoWordPress:trunkfrom
chriszarate:fix/sync-post-meta-prepared-queries

Conversation

@chriszarate
Copy link

@chriszarate chriszarate commented Mar 20, 2026

Opening this for conversation: What if we used direct database operations instead of post meta functions? Does that alleviate concerns about changing the APIs? What are the risks, aside from the security of the operations themselves?

Trac ticket: https://core.trac.wordpress.org/ticket/64696


AI usage

Unit tests written with assistance from Claude Opus 4.6.

@github-actions
Copy link

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props czarate.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@chriszarate chriszarate force-pushed the fix/sync-post-meta-prepared-queries branch from c6314bc to bfb5f12 Compare March 20, 2026 20:25
//
// This operation corresponds to this function call (single=false):
// add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
$meta_value = wp_unslash( $update );
Copy link
Contributor

@adamziel adamziel Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we remove the wp_unslash() call? Or is $update always slashed here? The method docstring doesn't say that $update must be slashed which makes me think wp_unslash() is present here because it's also present in add_metadata(). Is there any chance that $update could contain a legitimate \" byte sequence that should be preserved verbatim? Right now we'd lose that slash. It may be an important slash!

Also, should we skip the sanitize_meta call? I understand it is brought over from add_metadata, but what kind of sanitization would we expect the plugin authors to hook into here? Would it be safer to start without this extension point until a clear use-case arises? This data is being inserted into post meta today, but it may be moved to a separate table later on – if these sanitization hooks fire in here, we'll need to consider how to handle the registered hooks for inserts targeting the new database table. Not firing those hooks would make that easier later on.

As a side note, WordPress escaping and sanitization strategies always confuse me. Why would we sanitize anything for storage? It's just data. It needs different treatment depending on the context: raw processing, displaying in an HTML attribute, displaying in an HTML text node, embedding in JavaScript. We can't apply a context-specific treatment without damaging the data if the stored value is already preemptively treated with stripslashes or htmlentities. I don't mean that about your work in this PR specifically, it's just a series of weird choices WordPress makes all over the place. I believe WordPress becomes better when we avoid those choices in new code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I struggled with these questions. I coded it to match the existing functions mostly to pre-empt questions and spurious security concerns. But I think you're right that if we go with this approach, we should tailor it tightly to the use case.

Is there any chance that $update could contain a legitimate " byte sequence that should be preserved verbatim?

Here, no, it will always be Base-64 encoded binary data. In the awareness update, possibly. Good catch.

$meta_value = sanitize_meta( self::SYNC_UPDATE_META_KEY, $meta_value, 'post', self::POST_TYPE );
$meta_value = maybe_serialize( $meta_value );

$result = $wpdb->insert(
Copy link
Contributor

@adamziel adamziel Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dmsnell have you ever found a way to run native prepared statements via $wpdb? Or is concatenating strings via $wpdb->insert() the best alternative that $wpdb offers?

// add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
$meta_value = wp_unslash( $update );
$meta_value = sanitize_meta( self::SYNC_UPDATE_META_KEY, $meta_value, 'post', self::POST_TYPE );
$meta_value = maybe_serialize( $meta_value );
Copy link
Contributor

@adamziel adamziel Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need maybe_unserialize here? If it's in here because that's what add_metadata() do, not because we actually expect an already-serialized value, we could replace it with serialize(). Or, even better – with json_encode() as that would prevent an entire class of unserialize()-based vulnerabilities.

//
// This operation corresponds to this function call:
// update_post_meta( $post_id, self::AWARENESS_META_KEY, $awareness );
$meta_value = wp_unslash( $awareness );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

// Return true even if the database operation fails, since this operation is
// not critical for collaboration state.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is outside of scope of this PR. I'm just noticing it because it stood out to me.

Why not tell the caller whether the operation failed or succeeded and decide what to do? It's fine if it chooses to ignore the failure and it's fine if it chooses to do something about it. However, the current public API never offers that choice since it always communicates success.

Also, even more interestingly – a failure to insert or update returns true, but a failure to find the $post_id returns false. Why is one failure mode treated differently from the other one?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants