WP Manifestindependent plugin directory

o2 WP Registry grade D. High-severity findings · 6 findingsWP RegistryHigh-severity findings · 6 findingsOpen the reportD

The o2 plugin for WordPress — blogging at the speed of thought

★ 339stars
r133501-wpcomlatest · never

Install

Download source zip No release zip yet

This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.

Install straight from GitHub with WP-CLI:

wp plugin install https://github.com/automattic/o2/archive/refs/heads/master.zip
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

The o2 Plugin for WordPress - blogging at the speed of thought

Dependencies

This plugin requires WordPress 4.2 or higher, and PHP 5.3.6 or higher (due to a specific XML-parsing feature in use).

This plugin works with the p2-breathe theme, available at https://wpcom-themes.svn.automattic.com/p2-breathe

This plugin uses Genericons. If you're using Jetpack, then you should already have them available. If you're not using Jetpack, you should install and activate the Genericon'd plugin, available at https://wordpress.org/plugins/genericond/

This plugin's editor can insert code shortcodes. To get those to render as code blocks, you should install and activate the SyntaxHighlighter Evolved plugin, available at https://wordpress.org/plugins/syntaxhighlighter/

Installation

Upgrade to at least WordPress 4.2

Install the p2-breathe theme into wp-content/themes/p2-breathe

Activate the theme.

Install and activate the Genericon'd plugin.

Install and activate the SyntaxHighlighter Evolved plugin

Install this plugin into wp-content/plugins/o2

Activate this plugin

Caveats

Multisite self-hosted installs have not yet been tested and may not work.

Releases

2 releases.

Tag
Published
r133501-wpcom latest
Mar 30, 2016 10 years ago
Mar 30, 2016 10 years ago

These releases are tags only. The author does not attach a packaged zip, so there are no download counts to report.

D grade

Security

o2 0.2 · audited by WP Registry

High-severity findings.

2 high 4 medium
Audited release
0.2
Findings
6
Worst severity
high
Content hash
be52c55900d5f8a38d353def…

Findings

  • high Unauthenticated poll API dumps trashed posts and private-post comments via unsanitized WP_Query vars

    information_disclosure

    wp_ajax_nopriv_o2_read method=poll has no nonce or capability check. get_posts() copies $_REQUEST['queryVars'] into WP_Query after only forcing post_status to publish+trash (private added if the caller can read_private_posts). wp_parse_args leaves attacker-supplied suppress_filters, posts_per_page, post_type, p, meta_query, etc. in place, so suppress_filters=1 & posts_per_page=-1 disables the 24-hour posts_where window and returns every published and trashed post. Each row is serialized with o2_Fragment::get_fragment() which includes contentRaw (unfiltered post_content), titles, and nested comments. get_comments() is a site-wide get_comments({status:approve}) plus trash comments keyed by o2_comment_gmt_modified; WP_Comment_Query does not join parent post_status, so approved comments on private/draft/password posts from the last day are returned to anonymous callers. is_ok_to_add() only skips password-protected posts (and $is_page is undefined in that helper, so pages with passwords are also skipped — drafts still leak via their comments). Observable: POST admin-ajax.php?action=o2_read&method=poll with queryVars[suppress_filters]=1, queryVars[posts_per_page]=-1 yields JSON fragments of trashed posts that are not public HTML.

    inc/read-api.php:283-316

    Recommendation

    Do not pass client queryVars into WP_Query. Whitelist keys (author, cat, tag, p, pagename, s, paged) and coerce types. Drop 'trash' from anonymous post_status. For comments, restrict to comments whose parent post is publicly readable (join posts and require publish, or post_status=publish on WP_Comment_Query). Ignore suppress_filters, posts_per_page, post_type, meta_query from the request. Cap posts_per_page.

  • high Any authenticated user can upload files; write_upload never checks upload_files

    missing_capability

    wp_ajax_o2_write (auth only) accepts message.type=upload after wp_verify_nonce('o2_nonce'). write_upload() then loops $_POST['num_files'] and calls media_handle_upload() with no current_user_can('upload_files') or edit_posts check. The nonce is localized for every front-end visitor in o2Config.options.nonce, so a Subscriber who can comment (comment editor is shown to logged-in users) can POST the same payload the drag-and-drop editor sends. media_handle_upload does not check caps itself; attachments are created unattached (parent 0) in the media library using WordPress's default allowed mimes (PHP still blocked, but images/docs/audio/video are not). num_files is not bounded. JS mime checks in editor.js are client-side only. Contrast: core wp_ajax_upload_attachment checks upload_files or edit_post.

    inc/write-api.php:48-50

    Recommendation

    Return die_failure unless current_user_can('upload_files'). Optionally also require edit_posts or publish_posts so only users who can put the media in a post can upload. Cast and cap num_files. Keep the nonce check.

  • medium Unauthenticated preview runs attacker HTML through the_content (oEmbed/shortcodes)

    ssrf

    o2_Read_API::preview() is reachable as wp_ajax_nopriv_o2_read&method=preview with no nonce. For type=post it wp_unslash()es $_REQUEST['data'], runs generate_title() (which do_shortcode()s the first line), then apply_filters('the_content', ...). Core hooks WP_Embed::run_shortcode / autoembed and do_shortcode on the_content, so a URL-only body or [embed] URL causes the server to HTTP-fetch the target (wp_oembed_get → discover). On modern WP that fetch is wp_safe_remote_get (private IPs blocked); on the plugin's stated 4.2 minimum the oEmbed stack is weaker. Comment preview runs comment_text on the same unauthenticated input. The JSON HTML is then inserted with jQuery.html() in the caller's editor (self-XSS, not stored). The server-side effect is unauthenticated shortcode/embed execution and outbound requests.

    inc/read-api.php:460-494

    Recommendation

    Require a logged-in user (and o2_nonce) for preview. Do not run generate_title/do_shortcode/the_content on unauthenticated input; use a kses'd static preview or only the filters needed for display. Strip embed/autoembed for preview.

  • medium Stored XSS via post titles: unescaped {{{ titleFiltered }}} and jQuery.html()

    xss_stored

    Post titles are stored without wp_kses (WordPress does not kses titles on save). o2_Fragment applies the_title and puts the result in titleFiltered. inc/tpl/post.php interpolates {{{ data.titleFiltered }}} (unescaped), inc/tpl/post-view.php does the same for prev/next titles, and js/views/post.js assigns $('.o2-app-page-title').html(jsonifiedModel.titleFiltered) on pages. jQuery.html() and Backbone triple-stash execute event-handler markup (e.g. <img src=x onerror=...>) in every visitor's origin; <script> tags would not run via innerHTML but onerror/svg will. Write path: o2_Write_API::write_post create/update copies $message->titleRaw with only publish_posts / edit_post — Author role on a default site. Subscribers cannot publish through this API.

    inc/tpl/post.php:19-22

    Recommendation

    Escape titles for HTML in templates ({{ titleFiltered }} not {{{ }}}) and use .text() instead of .html() for the page title. Optionally kses or sanitize_text_field titleRaw in write_post before wp_insert_post/wp_update_post.

  • medium Author role can rewrite checklist lines on any post or comment

    missing_capability

    wp_ajax_o2_checklist (auth) verifies o2_nonce then current_user_can_edit_checklist(). After the normal edit_post/edit_comment checks, any user whose WP_User->roles contains 'author' returns true for every object ID. update_object_content() then bump_post() / wp_update_comment() on that object, rewriting matching task lines (delete/update/add/check/move). Authors on a default install may only edit their own posts; this bypass lets them alter other authors' post_content and any comment's comment_content wherever a line matches the task regex. Subscribers and contributors do not get the bypass. The extra grant is explicit in the helper, not an inverted admin conjunction.

    modules/checklists/load.php:72-93

    Recommendation

    Remove the blanket author-role grant. Require current_user_can('edit_post', $id) or current_user_can('edit_comment', $id) only (optionally plus moderate_comments for others' comments). Keep the nonce check.

  • medium CSRF: any logged-in user hitting themes.php?activated= disables comment moderation

    csrf

    on_activating_o2_compatible_theme() runs on admin_init whenever REQUEST_URI contains /wp-admin/themes.php and activated= and the current theme supports o2. There is no current_user_can('switch_themes') and no nonce. admin_init fires before the themes.php cap failure, so a Subscriber (or a CSRF'd Administrator) loading GET /wp-admin/themes.php?activated=1 causes delete_option('comment_whitelist') (the 'comment author must have a previously approved comment' switch, later comment_previously_approved), update_option('thread_comments','1'), thread_comments_depth=10, Jetpack comment likes, and insertion of Search/Filter/Live Comments widgets into sidebar-1. This is not limited to a real theme-switch; any activated= query string retriggers it.

    o2.php:986-1012

    Recommendation

    Require current_user_can('switch_themes') (or manage_options) and a one-time activation flag (the existing o2-just-activated option) instead of an unauthenticated-looking activated= query string. Do not delete comment_whitelist as a side effect of a GET.

WP Registry hashes the installable build and reports on that exact bytes-for-bytes copy. Embargoed findings are withheld until they are disclosed, so a clean verdict means nothing public is outstanding. WP Manifest does not audit code itself.