Last active
September 19, 2025 04:25
-
-
Save dknauss/45d2185376aaca40fac5f92f3e3bbc85 to your computer and use it in GitHub Desktop.
"Psudo" is a "pseudo sudo" mode for WordPress administrators: it requires reauthentication to perform sensitive admin tasks for a short window of time. (This overgrown gist is due to become a real plugin.)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * == Psudo == | |
| * | |
| * Plugin Name: Psudo | |
| * Version: 1.0 | |
| * Author: Dan Knauss | |
| * Contributors: | |
| * Donate link: https://example.com/ | |
| * Tags: security, user management | |
| * Requires at least: | |
| * Tested up to: | |
| * Stable tag: | |
| * Requires PHP: | |
| * License: GPLv2 or later | |
| * License URI: https://www.gnu.org/licenses/gpl-2.0.html | |
| * Text Domain: psudo | |
| * Description: Psudo requires Administrators to log in again for temporary unrestricted access to Users, Tools, Settings, Themes, and Plugins. | |
| * | |
| * == Description == | |
| * | |
| * Psudo requires Administrators to log into their accounts again after being forcibly logged out when they try to access Users, Tools, Settings, Themes, and Plugins — unless they've | |
| * already reauthenticated within a period of time (1-30 minutes) defined in the plugin settings. (The default is 10 minutes.) By this method, Psudo effectively creates a "pseudo sudo" | |
| * (Superuser Do) or "Psudo" session mode for Administrators. | |
| * | |
| * Psudo can help you harden the security of your WordPress site against the threat of compromised user accounts and make your Admins think twice about their privileges and workflows. | |
| * | |
| * == What problem does this plugin solve? == | |
| * | |
| * Psudo acts as a last line of defense in scenarios where an Administrator account or privileges have been accessed by an attacker with a rogue, stolen, or hijacked account. | |
| * | |
| * For example: | |
| * | |
| * - An admin user has remained logged in on a public or stolen device, allowing an attacker to continue using their active session on that device. | |
| * - An admin user logs into their account on a compromised device or network that allows attackers to harvest and use their active session. | |
| * - An admin user falls victim to a Session Fixation or other Session Hijacking attack. | |
| * - An attacker possesses a non-administrator account and exploits a vulnerability (e.g., broken access controls) in third-party code (a theme or plugin) that enables privilege escalation. | |
| * | |
| * Conceptually, Psudo is a highly granular application of the Principle of the Least Privilege (PotLP). Administrators generally do not require constant direct access to operate the theme/plugin | |
| * installer/updater, or the core application settings for WordPress. When an admin user needs to use these capabilities, a quick re-authentication ensures they are who they say they are. This | |
| * brief "window of trust" must constantly be re-opened even if the absolute session duration remains 2 to 14 days, which are the system defaults. This is a good security discipline for admin | |
| * users. Psudo can help normalize user expectations about security for WordPress that resembles familiar UX patterns for reauthentication in the most widely used operating systems. | |
| * | |
| * Please take note, Psudo is not a robust, comprehensive, standalone security solution. Most WordPress sites that are breached are compromised by attackers through vulnerabilities in obsolete | |
| * third-party plugins and other code that has broken access controls, is susceptible to abuse through unfiltered query strings, missing nonces, and/or insufficiently secured Admin-AJAX | |
| * calls, XML-RPC and REST API endpoints. Psudo does not attempt to comprehensively address these common attack vectors. It's always imperative for security that you keep all your installed code | |
| * updated within a reasonably hardened server and application environment. | |
| * | |
| * To defend these common attack vectors, take care to ensure the quality of any custom or third-party code you install with WordPress. Always make timely updates to WordPress core, plugins, | |
| * themes, and extensions! You should also use Two-Factor/Multifactor Authentication (2FA/MFA), absolute session limits, limits on API connections, allowlists/device or IP restrictions for | |
| * administrators, dashboard restriction for all users who don't need it, and activity logging and alerts. | |
| * | |
| * Recommended plugins for these purposes: | |
| * | |
| * - [Two-Factor](https://en-ca.wordpress.org/plugins/two-factor/) | |
| * - [WebAuthn Provider for Two Factor](https://en-ca.wordpress.org/plugins/two-factor-provider-webauthn/) | |
| * - [Remember Me Controls](https://en-ca.wordpress.org/plugins/remember-me-controls/) | |
| * - [Remove Dashboard Access](https://en-ca.wordpress.org/plugins/remove-dashboard-access-for-non-admins/) | |
| * - [Restricted Site Access](https://en-ca.wordpress.org/plugins/restricted-site-access/) | |
| * - [WP fail2ban](https://wordpress.org/plugins/wp-fail2ban/) | |
| * - [Disable REST API](https://wordpress.org/plugins/disable-json-api/) | |
| * - [Disable XML-RPC-API](https://wordpress.org/plugins/disable-xml-rpc-api/) | |
| * - [Stream](https://wordpress.org/plugins/stream/) | |
| * | |
| * You should also consider restricting the number of true Administrators to one on most sites — an account rarely used, as in "break-glass" emergency scenarios only. A custom role between Editor | |
| * and Administrator who cannot `manage_options` may be adequate. Then, you could load all roles and privileges from a static file (possibly an `mu-plugin`) and lock or eliminate `wp_user_roles` | |
| * in the `wp_options` table. — or monitor it for changes as a "canary" to detect an intrusion attempt. | |
| * | |
| * Psudo will be most effective (offering greater security benefits) if it's run as an mu-plugin (Must-Use Plugin) at the very beginning of the initialization seuquence. Consider using Felix | |
| * Arntz's WP Plugin MU Loader](https://gist.github.com/felixarntz/daff4006112b60dfea677ca08fc0b31c). | |
| * | |
| * == Changelog == | |
| * | |
| * = 1.0 = | |
| * Initial release. | |
| * | |
| * == Upgrade Notice == | |
| * = 1.0 = | |
| * Initial release. | |
| * | |
| **/ | |
| add_action( 'init', 'psudo_conditions' ); // Should run as early as possible or as an mu-plugin. | |
| /** | |
| * Defines and checks for conditions that will trigger a forced logout for the user if those conditions are met. | |
| * | |
| * This function checks if the user is logged in and has the 'manage_options' capability. If the user meets these criteria, it checks the user's last | |
| * last forced logout time, which is a timestamp logged by Psudo in the user's metadata. If the current time has passed the Psudo session expiration — | |
| * i.e., the limit specified in this function: 10 minutes after the last forced logout — then the user will be forcibly logged out if they access a | |
| * restricted URL. (Restricted URLs are also specified in this function.) Then the user will be presented with the login form and a re-authentication | |
| * request. Following re-authentication, the user will be redirected to the last (restricted) page they tried to access. | |
| * | |
| * Note that a user who is forcibly logged out and then delays logging back in may have little or no time to exercise their administrator privileges | |
| * before they are forced to log in again. The last forced logout time starts the Psudo session clock and must be close in time to the next login for | |
| * that new session to be viable for more than a few minutes of privileged admin actions. | |
| * | |
| * @since 1.0.0 | |
| * @return void | |
| */ | |
| function psudo_conditions() { // Force Logout Conditions function implementation. | |
| if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { // If the user is logged in and has admin capabilities, let's take a closer look. | |
| return; // (Ignore everyone else.) | |
| } | |
| $user_id = get_current_user_id(); // Get the current user's ID. | |
| $last_forced_logout = get_cached_user_meta($user_id, 'last_forced_logout'); // Get the current user's last forced logout time. | |
| if ( ! $last_forced_logout ) { // If the user's last forced logout time doesn't exist, is NULL, or zero, | |
| psudo_force_logout_and_redirect( $user_id ); // then force a logout. | |
| } | |
| $psudo_session_limit = get_option('psudo_session_limit', 10); // Get the Psudo session limit (default = 10) from settings saved in option table. | |
| $psudo_limit_since_last_forced_logout = $last_forced_logout + $psudo_session_limit * MINUTE_IN_SECONDS; | |
| // Define restricted URLs to trigger Psudo condition check. | |
| $restricted_urls = array( 'plugins.php', 'themes.php', 'options-general.php', 'users.php', 'tools.php' ); | |
| $current_url = filter_input(INPUT_SERVER, 'REQUEST_URI', FILTER_SANITIZE_URL ); // Clean and safe way to get current URL from PHP environment. | |
| foreach ( $restricted_urls as $restricted_url ) { // Check for conditions where we should force a logout: | |
| if ( strpos( $current_url, $restricted_url ) !== false && // - Has the user has requested a URL that is restricted? | |
| time() > $psudo_limit_since_last_forced_logout ) { // - Has the current time passed the limit based on the user's last forced logout? | |
| psudo_force_logout_and_redirect( $user_id, $current_url ); // If both conditions are true, force a logout. | |
| } | |
| } | |
| } | |
| /** | |
| * Force a logout after logging the current time as the last_forced_logout time in user_meta. Display an explanatory message on the login screen. \ | |
| * When the user logs back in, redirect them to their last requested URL. | |
| * | |
| * | |
| * @since 1.0.0 | |
| * @param int $user_id The ID of the user to force logout. | |
| * @param string $current_url The URL to redirect to after logout. | |
| * @return void | |
| */ | |
| function psudo_force_logout_and_redirect( $user_id, $current_url ) { // Force Logout and Redirect function implementation. | |
| $new_value = time(); // Get current time as variable to pass to cache. | |
| update_cached_user_meta($user_id, 'last_forced_logout', $new_value); // Update the cached last forced logout time with the current time. | |
| wp_logout(); // Force logout. | |
| wp_redirect( wp_login_url() . '?' . build_query(array( // Build the redirect so the user will return to their last URL after logging in. | |
| 'redirect_to' => $current_url, // Make the redirect_to URL the last URL the user requested before they were logged out. | |
| 'reauth_now' => 'true' // Set the reauth_now URL parameter as a flag for customizing the reauth login screen. | |
| )) ); | |
| exit; // Terminate PHP script execution. | |
| } | |
| add_filter( 'login_message', 'psudo_reauth_message' ); // Re-authentication Message login form filter implementation. | |
| /** | |
| * Display a custom message above the login form when the proper key ('reauth') and value ('true') pair are included in the URL parameters. | |
| * This happens when the user is forcibly logged out by Psudo in the previous function. It's intended to provide some information to | |
| * the user as to why they've been logged out and what should happen next. | |
| * | |
| * Note we can't use get_query_var or WP global variables to get the current URL following a redirect+exit, so we must use PHP for this, just not superglobals. | |
| * | |
| * @since 1.0.0 | |
| * @param string $message The existing login message. | |
| * @return string The modified login message. | |
| */ | |
| function psudo_reauth_message( $message ) { // Session Ended Message function implementation. | |
| if ( filter_input(INPUT_GET, 'reauth_now', FILTER_SANITIZE_STRING) === 'true' ) { // If the "reauth" parameter exists in the current URL and is "true" then return the following message. | |
| $message .= '<p class="message"><strong>' . esc_html__( 'With great power comes great responsibility.', 'psudo' ) . | |
| '</strong><br/>' . esc_html__( 'Reauthenticate to continue where you were. 🥪', 'psudo'). '</p>'; | |
| } | |
| return $message; | |
| } | |
| /** | |
| * Retrieve cached user metadata. | |
| * | |
| * @param int $user_id The user ID. | |
| * @param string $meta_key The metadata key. | |
| * @param bool $force_refresh Whether to force refresh the cache or not. | |
| * @return mixed The user metadata or false on failure. | |
| */ | |
| function get_cached_user_meta($user_id, $meta_key, $force_refresh = false) { | |
| // Generate a unique key for the transient. | |
| $transient_key = 'user_meta_' . $user_id . '_' . $meta_key; | |
| // Try to get the value from the transient cache. | |
| if (!$force_refresh) { | |
| $cached_value = get_transient($transient_key); | |
| if ($cached_value !== false) { | |
| return $cached_value; | |
| } | |
| } | |
| // If not found in cache, get the value from the database. | |
| $meta_value = get_user_meta($user_id, $meta_key, true); | |
| // Store the value in the transient cache for future requests. | |
| set_transient($transient_key, $meta_value, 12 * HOUR_IN_SECONDS); // Cache for 12 hours. | |
| return $meta_value; | |
| } | |
| /** | |
| * Update user metadata and refresh cache. | |
| * | |
| * @param int $user_id The user ID. | |
| * @param string $meta_key The metadata key. | |
| * @param mixed $meta_value The new metadata value. | |
| * @return bool True on successful update, false on failure. | |
| */ | |
| function update_cached_user_meta($user_id, $meta_key, $meta_value) { | |
| // Update the metadata in the database. | |
| $result = update_user_meta($user_id, $meta_key, $meta_value); | |
| if ($result) { | |
| // Generate a unique key for the transient. | |
| $transient_key = 'user_meta_' . $user_id . '_' . $meta_key; | |
| // Refresh the cache with the new value. | |
| set_transient($transient_key, $meta_value, 12 * HOUR_IN_SECONDS); // Cache for 12 hours. | |
| } | |
| return $result; | |
| } | |
| /** | |
| * Add settings page for Psudo plugin. | |
| */ | |
| function psudo_add_admin_menu() { | |
| add_options_page( | |
| 'Psudo Settings', // Page title | |
| 'Psudo', // Menu title | |
| 'manage_options', // Capability | |
| 'psudo-settings', // Menu slug | |
| 'psudo_settings_page' // Callback function | |
| ); | |
| } | |
| add_action( 'admin_menu', 'psudo_add_admin_menu' ); | |
| /** | |
| * Register settings for Psudo plugin. | |
| */ | |
| function psudo_register_settings() { | |
| register_setting( 'psudo_options', 'psudo_session_limit', array( | |
| 'type' => 'integer', | |
| 'sanitize_callback' => 'psudo_sanitize_session_limit', | |
| 'default' => 10, | |
| ) ); | |
| add_settings_section( | |
| 'psudo_settings_section', // ID | |
| 'How long should a Psudo session last?', // Title | |
| '__return_null', // Callback | |
| 'psudo-settings' // Page | |
| ); | |
| add_settings_field( | |
| 'psudo_session_limit', // ID | |
| 'Psudo session limit in minutes', // Title | |
| 'psudo_session_limit_callback', // Callback | |
| 'psudo-settings', // Page | |
| 'psudo_settings_section' // Section | |
| ); | |
| } | |
| add_action( 'admin_init', 'psudo_register_settings' ); | |
| /** | |
| * Sanitize callback for session limit. | |
| * | |
| * @param mixed $input The input value. | |
| * @return int The sanitized input value. | |
| */ | |
| function psudo_sanitize_session_limit( $input ) { | |
| $input = intval( $input ); | |
| if ( $input < 1 || $input > 30 ) { | |
| add_settings_error( | |
| 'psudo_session_limit', | |
| 'psudo_session_limit_error', | |
| 'Psudo Session Limit must be between 1 and 30 minutes.', | |
| 'error' | |
| ); | |
| return get_option( 'psudo_session_limit', 10 ); // Return the default value if input is invalid. | |
| } | |
| return $input; | |
| } | |
| /** | |
| * Callback function for session limit field. | |
| */ | |
| function psudo_session_limit_callback() { | |
| $psudo_session_limit = get_option( 'psudo_session_limit', 10 ); | |
| echo '<input type="number" id="psudo_session_limit" name="psudo_session_limit" value="' . esc_attr( $psudo_session_limit ) . '" min="1" max="30" />'; | |
| } | |
| /** | |
| * Settings page callback function. | |
| */ | |
| function psudo_settings_page() { ?> | |
| <div class="wrap"> | |
| <h1>Psudo Settings</h1> | |
| <p>Psudo requires users with Administrator privileges to reauthenticate for temporary unrestricted access to Settings, Themes, and Plugins.</p> | |
| <p>Here you can define how long the temporary unrestricted Sudo-like (superuser) session will last.</p> | |
| <p>Choose a whole number between 1 and 30 minutes.</p> | |
| <p>The default is 10 minutes.</p> | |
| <form method="post" action="options.php"> | |
| <?php | |
| settings_fields( 'psudo_options' ); | |
| do_settings_sections( 'psudo-settings' ); | |
| submit_button(); | |
| ?> | |
| </form> | |
| </div> | |
| <?php } ?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment