WP Manifestindependent plugin directory
manifest / editor / hm-table-of-contents-plugin

HM TOC WP Registry grade D+. High-severity findings · 2 findingsWP RegistryHigh-severity findings · 2 findingsOpen the reportD+

WordPress plugin that adds anchors to headings in post content and generates table of contents from headings.

★ 4stars
4.7kcomposer installs
1.1.7latest · 1 year 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/humanmade/hm-table-of-contents-plugin/archive/refs/heads/main.zip

On Packagist as humanmade/hm-toc:

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

HM Table of Contents

WordPress plugin with the following features:

  • Generate table of contents that lists all headings (Ordered hierachically) in post content.
  • Automatically add anchors links after all headings in post content.
  • Block to add these to pages and template.
  • Adds 'active' class to the current heading in the table of contents.

FAQ

I don't want to automatically add anchor links to headings.

You can remove the filter that adds these with the following snippet. Doing it like this means that the plugin will still ensures headings all have IDs.

// Don't append an anchor link to each heading.
add_filter( 'hm_toc.contents.anchor_html', '__return_empty_string' );

Releases

9 releases.

Tag
Published
1.1.7 latest
Nov 27, 2024 1 year ago
Nov 25, 2024 1 year ago
Sep 27, 2024 2 years ago
Sep 27, 2024 2 years ago
Sep 23, 2024 2 years ago
Sep 20, 2024 2 years ago
Sep 20, 2024 2 years ago
Sep 13, 2024 2 years ago
Sep 6, 2024 2 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

HM TOC 1.1.7 · audited by WP Registry

High-severity findings.

2 high
Audited release
1.1.7
Findings
2
Worst severity
high
Content hash
babf1e8cdb031e2695413056…

Findings

  • high Public REST `contents` field leaks headings from password-protected pages

    information_disclosure

    register_rest_field() adds a `contents` property to every page REST response. The get_callback does get_post($data['id']) and returns get_header_hierarchy($post), which reads $post->post_content directly and returns title/href/items for every h1–h4. Core still allows unauthenticated GET /wp/v2/pages/{id} for published password-protected pages, but blanks content.rendered until the password is supplied. This field is not similarly gated: there is no post_password_required($post), can_access_password_content, or current_user_can('edit_post') check, and no update_callback so the field is read-only but always populated. Observable consequence: an anonymous client can list published pages, detect content.protected===true, and still recover the full heading outline of the gated body (titles plus heading ids). Private/draft pages remain hidden by core's check_read_permission; the bypass is the password layer on published pages.

    inc/namespace.php:71-77

    Recommendation

    In the get_callback, load the post and return an empty array when post_password_required($post) is true for the current request (or when the user cannot edit_post / the REST password was not accepted). Mirror core's can_access_password_content() behavior used for content.rendered. Do not parse post_content for statuses the requester cannot read.

  • high TOC block renders heading outlines without a post-read or password check

    information_disclosure

    The dynamic block hm/table-of-contents (block/build/render.php, registered from block.json render) resolves $post_id from attributes['postId'] or $block->context['postId'], calls get_post() with no capability or post_password_required() check, then prints get_header_hierarchy() via the_heading_list(). Two concrete consumers: (1) advertised template use — when the block is in a block theme page/single template, context.postId is the queried page; an unauthenticated visitor of a password-protected page sees core's password form from the_content but still gets the heading list from raw post_content; (2) the postId number attribute is stored in block markup and is not validated, so a user who can publish (Author+) can put <!-- wp:hm/table-of-contents {"postId":N} /--> on a public post and disclose headings of draft/private/password-protected posts, and Contributor+ can obtain the same HTML via core's block-renderer REST endpoint with attributes[postId]=N. the_heading_list() also echoes title/href without esc_html/esc_attr (unlike inc/template-tags.php); titles are wp_strip_all_tags()'d so this is defense-in-depth, not a separate XSS.

    block/build/render.php:7-44

    Recommendation

    Before calling get_header_hierarchy(), require the current user to be allowed to read that post: skip rendering when post_password_required($post) or when the post is not publicly viewable and the user lacks read_post. Ignore or cap attributes['postId'] so a block on post A cannot load post B unless the viewer can read B. Escape heading titles with esc_html() and hrefs with esc_url()/esc_attr() in the_heading_list() to match Template_Tags\render_items().

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.