WP Manifestindependent plugin directory
manifest / content / papi

Papi WP Registry grade D. High-severity findings · 7 findingsWP RegistryHigh-severity findings · 7 findingsOpen the reportD

:rocket: WordPress Page Type API with custom fields

★ 260stars
30kcomposer installs
v3.2.0latest · 9 years ago

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/wp-papi/papi/archive/refs/heads/master.zip

On Packagist as wp-papi/papi:

composer require wp-papi/papi
No built-in updater. New versions won't appear under Dashboard → Updates. Install new releases yourself, or let Git Updater track the repository.

Papi

master is unsafe. 3.x is the stable branch.

Papi has a different approach on how to work with fields and page types in WordPress. The idea is coming from how Page Type Builder in EPiServer works and has been loved by the developers.

So we though why don’t use the same approach in WordPress? Papi is today running in production and has been easy to work with when it came to add new fields. Papi don’t have any admin user interface where you should add all fields, we use classes in PHP, where one class represents one page type and in your class you add all fields you need. It’s that easy!

Visit Papi’s project page

A message from the author

v3.2.0 will likely be the last release I plan to work on as the core maintainer of Papi since my focus has shifted from WordPress to doing more JavaScript and Go projects. I hope you understand my decision to step back from the project, if you have any questions or would be interested in take over some of the maintenance of the project please let me know. I will still be around answering questions and helping any new maintainers. Some bug fixes and/or pull request may be added (but no new versions) since me and my colleagues use Papi internally and will be continuing doing it.

Installation

If you're using Composer to manage WordPress, add Papi to your project's dependencies. Run:

composer require wp-papi/papi

Or manually add it to your composer.json:

"require": {
  "php": "^^7.4",
  "wordpress": "^4.6",
  "wp-papi/papi": "^3.2"
}

Build CSS and JavaScript

Install dependencies:

make deps

Build CSS:

make css

Build JavaScript:

make js

Testing

Visit the readme file for testing.

Coding style

You can check if your contribution passes the styleguide by installing PHP CodeSniffer and running the following in your Papi directory:

make lint:php

Contributing

Visit the contributing file.

Security

If you discover a security vulnerability within this package, please send an e-mail to Fredrik Forsmo at security@frozzare.com. All security vulnerabilities will be promptly addressed.

License

MIT © Fredrik Forsmo

Releases

Newest 25 of 60 recorded releases.

Tag
Published
v3.2.0 latest
Sep 7, 2017 9 years ago
Jun 19, 2017 9 years ago
Apr 5, 2017 9 years ago
Feb 14, 2017 9 years ago
Dec 16, 2016 9 years ago
Nov 1, 2016 9 years ago
Oct 26, 2016 9 years ago
Oct 6, 2016 10 years ago
Sep 23, 2016 10 years ago
Sep 12, 2016 10 years ago
Sep 5, 2016 10 years ago
Sep 5, 2016 10 years ago
Aug 30, 2016 10 years ago
Aug 16, 2016 10 years ago
Aug 10, 2016 10 years ago
Aug 10, 2016 10 years ago
Aug 8, 2016 10 years ago
Aug 8, 2016 10 years ago
Aug 2, 2016 10 years ago
Jul 27, 2016 10 years ago
Jul 27, 2016 10 years ago
Jul 4, 2016 10 years ago
Jun 27, 2016 10 years ago
Jun 20, 2016 10 years ago
Jun 20, 2016 10 years ago
All releases on GitHub →

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

D grade

Security

Papi 3.2.0 · audited by WP Registry

High-severity findings.

5 high 1 medium 1 low
Audited release
3.2.0
Findings
7
Worst severity
high
Content hash
eac7515ea6ebdb6e289d8273…

Findings

  • high Subscriber+ AJAX renders attacker-built properties and invokes PHP callables

    rce

    Papi_Admin_Ajax is constructed on every request (not admin-gated) and handle_papi_ajax runs on parse_request. The only gate is is_user_logged_in(); there is no nonce or capability check. get_property copies every default property option from $_GET (papi_get_qs does not sanitize arrays) into papi_property(); get_properties json_decodes $_POST['properties'] the same way. Rendering calls papi_maybe_get_callable_value() on before_html and after_html (explicitly excluded from papi_esc_html) and, for type=html, on settings.html. is_callable strings such as phpinfo are executed via call_user_func_array with output buffering, and the captured stdout is returned as JSON html. A Subscriber can GET /papi-ajax/?action=get_property&type=string&before_html=phpinfo or POST action=get_properties with {"type":"html","settings":{"html":"phpinfo"}} and receive a full phpinfo dump. Any no-arg PHP/WordPress callable is similarly invocable as the web user.

    src/admin/class-papi-admin-ajax.php:62-86,135-203

    Recommendation

    Require current_user_can('edit_posts') (or a dedicated Papi cap) and a nonce on every papi/ajax action. Do not pass request-supplied before_html/after_html/html/settings into is_callable/call_user_func*. Allowlist property types and option keys; treat HTML options as escaped strings only.

  • high Subscriber+ AJAX executes arbitrary shortcodes via do_shortcode

    unsafe_input

    get_shortcode reads the shortcode query argument, html_entity_decodes and wp_unslash()es it, then returns do_shortcode($shortcode) as JSON. The handler is registered on the same login-only dispatcher (no nonce, no capability, reachable from the frontend at /papi-ajax/?action=get_shortcode). A Subscriber can invoke any shortcode registered by WordPress core, the theme, or other plugins, including Papi's own [papi_field id=N slug=...] and [papi_option slug=...] which read post/option field values with no capability check, plus [embed] (SSRF/oEmbed). This is the same class of authenticated arbitrary shortcode execution that WordPress core patched in 6.3.2 (parse_media_shortcode). Observable: GET /papi-ajax/?action=get_shortcode&shortcode=[papi_field id=1 slug=api_key] returns the field value.

    src/admin/class-papi-admin-ajax.php:248-261

    Recommendation

    Remove get_shortcode or restrict it to an allowlist of shortcodes the current user is allowed to preview (e.g. only the URL/file property's expected embed). Require edit_posts plus a nonce. Never pass raw request input to do_shortcode.

  • high Subscriber+ AJAX WP_Query dumps private/draft posts and post_password

    information_disclosure

    get_posts array_merges attacker-controlled query (from GET query[]) over a WP_Query default, then optionally projects attacker-chosen WP_Post fields. Later keys win, so query[post_type]=any and query[post_status]=private,draft,pending,future override the defaults. WP_Query does not apply read_private_posts when post_status is explicitly set (perm=readable is not). fields[]=post_content&fields[]=post_password copies those object properties into the JSON response. A Subscriber can therefore list unpublished content, revision bodies (post_type=revision&post_status=inherit), and password-protected post passwords. posts_per_page=-1 dumps the table. Same login-only dispatcher; no nonce.

    src/admin/class-papi-admin-ajax.php:88-128

    Recommendation

    Do not accept arbitrary WP_Query arguments from the request. Hardcode post_type from the property, force post_status=publish (or use perm=readable), and only return ID and post_title. Require edit_posts and a nonce. Cap posts_per_page.

  • high Option save checks nonce only; Contributor+ can update arbitrary options

    missing_capability

    Papi_Admin_Option_Handler::save_properties runs on admin_init for POST when papi_get_meta_type()==='option'. papi_get_meta_type() returns $_GET['meta_type'] first, so any admin POST to wp-admin/?meta_type=option qualifies. The only gate is wp_verify_nonce(papi_meta_nonce, 'papi_save_data') — there is no manage_options / option-type capability check even though Papi_Option_Type::$capability defaults to manage_options. The same nonce is printed on every post/term/attachment editor (edit_form_after_title / attachment_fields_to_edit), so a Contributor who can open post.php already holds a valid nonce. POST keys matching /^papi_.*/ are paired with a client-supplied base64 JSON property object and passed to papi_data_update(..., 'option'), which calls update_option(unpapify($slug), $value). Crafting papi_default_role=administrator, papi_siteurl, papi_users_can_register, or papi_active_plugins is enough for site takeover. Observable: after the POST, get_option('default_role') is administrator.

    src/admin/class-papi-admin-option-handler.php:11-38

    Recommendation

    Require current_user_can('manage_options') (or the option type's capability) in addition to the nonce. Use a distinct nonce action for option pages. Ignore GET meta_type unless the user can manage options. Only update slugs that exist on a registered option type.

  • high Client-controlled property JSON can overwrite wp_posts columns on save

    missing_capability

    On save_post, after the shared papi_save_data nonce and an edit_posts (not edit_post / publish_posts) check, prepare_properties_data treats the hidden *_property field as a base64 JSON object describing the property. If that object has overwrite:true, the slug (unpapify of the papi_* POST key) is written through $wpdb->update($wpdb->posts, $this->overwrite, ['ID'=>$post_id]). The property definition is therefore attacker-controlled, not limited to PHP page-type classes. A Contributor saving their draft can POST papi_post_status=publish with papi_post_status_property=base64({"type":"string","overwrite":true}) and publish without publish_posts, or set post_author, post_password, post_type, or post_content via the posts table, bypassing wp_insert_post capability checks that already ran. Observable: the row's post_status becomes publish.

    src/admin/class-papi-admin-meta-handler.php:26-43,77-171

    Recommendation

    Do not trust client-supplied property JSON for overwrite or type. Resolve the property from the registered entry type by slug. If overwrite remains, allowlist column names (e.g. post_content) and require the matching meta cap (publish_posts, edit_others_posts). Use current_user_can('edit_post', $post_id).

  • medium Subscriber+ AJAX get_terms enumerates any taxonomy

    information_disclosure

    get_terms takes taxonomy and query[] from the query string with no capability or nonce check beyond is_user_logged_in(). If taxonomy_exists(), it calls get_terms($taxonomy, array_merge($query, ['fields'=>'id=>name'])). Attackers can set hide_empty=0, query non-public taxonomies, and pull term names/IDs that are not exposed on the frontend. Same /papi-ajax/?action=get_terms dispatcher as the other AJAX actions.

    src/admin/class-papi-admin-ajax.php:263-283

    Recommendation

    Allowlist taxonomies from the property settings. Force public query args (hide_empty, number). Require edit_posts and a nonce. Do not merge arbitrary get_terms arguments from the request.

  • low Plugin is unmaintained (v3.2.0 announced as last release)

    unmaintained

    The v3.2.0 GitHub release (this audited tag) states it will likely be the last release, with the maintainer stepping back from WordPress. The current GitHub README carries the unmaintained.tech badge. PHP requirement in the tagged composer.json is ^5.6 || ^7.0. High-severity AJAX and option-save issues in this version therefore have no upstream patch channel.

    papi-loader.php:3-12

    Recommendation

    Replace Papi with a maintained fields framework, or fork and patch the AJAX/option-save authorization issues before continuing to run this tag in production.

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.