PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / agents / guard.php

guard.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/agents/guard.php

208 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — Agents: authentication guard.
4 *
5 * Agents own a real `wp_users` row so capability checks, edit locks,
6 * comment attribution, and the standard WordPress audit trail work
7 * without a parallel ACL. The row is "synthetic" only in that no
8 * credential may ever resolve to it: an agent is invoked on the site's
9 * behalf, it never authenticates.
10 *
11 * This file is the security boundary for that claim, and it is the ONE
12 * part of the agents module that loads unconditionally — the rest of
13 * the module sits behind the `agents` extended option, but the blocks
14 * must not. Turning the feature off does not delete the agent rows, and
15 * a row whose login blocks unloaded with the feature is a row that
16 * accepts application passwords and password resets again.
17 *
18 * Two layers, because they cover different halves of WordPress:
19 *
20 * - The `authenticate` chain covers everything that presents a
21 * credential: wp-login.php, XML-RPC, and application passwords.
22 * - `determine_current_user` covers everything that presents a
23 * *session*: auth cookies, and any JWT / SSO / magic-link plugin
24 * that resolves a user id without going through `authenticate`.
25 * This is the one that matters, because the `authenticate` chain
26 * never runs for cookie validation — a third-party plugin calling
27 * `wp_set_auth_cookie( $agent_id )` would otherwise hand out a live
28 * agent session with no credential involved at all.
29 *
30 * `determine_current_user` deliberately does NOT interfere with the
31 * runner: `wp_set_current_user()` sets the global directly and never
32 * re-runs the filter, so switching into an agent to evaluate ability
33 * permissions still works exactly as before.
34 *
35 * @package OpenStation
36 */
37
38 defined( 'ABSPATH' ) || exit;
39
40 /**
41 * Marker meta key — the existence test for an agent.
42 *
43 * Lives here rather than in store.php because `openstation_agent_is_agent()`
44 * must resolve even when the agents module itself is not loaded.
45 *
46 * The VALUE keeps its pre-rebrand spelling on purpose: it is a
47 * persisted or externally-visible identifier, so renaming it would
48 * orphan data already written by live installs (or break a live
49 * URL). The mismatch between this constant's name and its value is
50 * deliberate — it is NOT a half-finished rename.
51 */
52 const OPENSTATION_AGENT_USER_MARKER_META = '_desktop_mode_agent';
53
54 /**
55 * Whether the given user is a OpenStation agent.
56 *
57 * @param int|WP_User|null $user User id or object.
58 * @return bool
59 */
60 function openstation_agent_is_agent( $user ) {
61 $user_id = $user instanceof WP_User ? $user->ID : (int) $user;
62 if ( $user_id <= 0 ) {
63 return false;
64 }
65 return '1' === (string) get_user_meta( $user_id, OPENSTATION_AGENT_USER_MARKER_META, true );
66 }
67
68 /**
69 * Block password authentication for agent users.
70 *
71 * Returns a `WP_Error` instead of the user object so wp-login.php
72 * surfaces the message inline. Covers XML-RPC and application passwords
73 * too — both authenticate through this same filter chain, at priority
74 * 20, below this callback.
75 *
76 * Note this only fires once an earlier callback already produced a
77 * `WP_User`, i.e. only when the presented credential was correct. That
78 * is deliberate: a wrong password still returns the generic core error,
79 * so this block never becomes a username-enumeration oracle.
80 *
81 * @param WP_User|WP_Error|null $user Current candidate from the chain.
82 * @return WP_User|WP_Error|null
83 */
84 function openstation_agent_block_authentication( $user ) {
85 if ( $user instanceof WP_User && openstation_agent_is_agent( $user ) ) {
86 return new WP_Error(
87 'openstation_agent_login_blocked',
88 __( 'This account is a OpenStation agent. Login is disabled.', 'desktop-mode' )
89 );
90 }
91 return $user;
92 }
93 add_filter( 'authenticate', 'openstation_agent_block_authentication', 30 );
94
95 /**
96 * Refuse to resolve an agent as the current user for a request.
97 *
98 * The catch-all. `authenticate` is only consulted when a credential is
99 * presented; `determine_current_user` is consulted on EVERY request that
100 * resolves an identity, so this covers auth cookies and every
101 * third-party token/SSO scheme that hooks the same filter — including
102 * ones that bypass `authenticate` entirely.
103 *
104 * Runs at `PHP_INT_MAX` so it is the last word regardless of what a
105 * token plugin registered.
106 *
107 * @param int|false $user_id User id resolved so far, or false.
108 * @return int|false
109 */
110 function openstation_agent_block_session( $user_id ) {
111 if ( $user_id && openstation_agent_is_agent( (int) $user_id ) ) {
112 return false;
113 }
114 return $user_id;
115 }
116 add_filter( 'determine_current_user', 'openstation_agent_block_session', PHP_INT_MAX );
117
118 /**
119 * Block password-reset emails for agent users.
120 *
121 * @param bool $allow Whether to allow the reset.
122 * @param int $user_id Target user id.
123 * @return bool
124 */
125 function openstation_agent_block_password_reset( $allow, $user_id ) {
126 if ( openstation_agent_is_agent( $user_id ) ) {
127 return false;
128 }
129 return $allow;
130 }
131 add_filter( 'allow_password_reset', 'openstation_agent_block_password_reset', 10, 2 );
132
133 /**
134 * Application passwords are the one credential that could authenticate
135 * a never-logs-in account over REST — refuse to make them available
136 * for agents, so the credential cannot be minted in the first place.
137 *
138 * `openstation_agent_block_authentication()` would reject it at use
139 * time anyway; this stops it existing.
140 *
141 * @param bool $available Whether application passwords are available.
142 * @param WP_User $user The user being checked.
143 * @return bool
144 */
145 function openstation_agent_block_application_passwords( $available, $user ) {
146 if ( $user instanceof WP_User && openstation_agent_is_agent( $user ) ) {
147 return false;
148 }
149 return $available;
150 }
151 add_filter( 'wp_is_application_passwords_available_for_user', 'openstation_agent_block_application_passwords', 10, 2 );
152
153 /**
154 * Suppress the password/email-changed notification emails for agents —
155 * the synthetic address is never delivered to, and a bounced
156 * notification per definition edit is pure noise in the mail log.
157 *
158 * @param bool $send Whether to send the notification.
159 * @param array $user The original user array before changes.
160 * @return bool
161 */
162 function openstation_agent_suppress_change_emails( $send, $user ) {
163 $user_id = is_array( $user ) && isset( $user['ID'] ) ? (int) $user['ID'] : 0;
164 if ( $user_id > 0 && openstation_agent_is_agent( $user_id ) ) {
165 return false;
166 }
167 return $send;
168 }
169 add_filter( 'send_password_change_email', 'openstation_agent_suppress_change_emails', 10, 2 );
170 add_filter( 'send_email_change_email', 'openstation_agent_suppress_change_emails', 10, 2 );
171
172 /**
173 * Send front-end author archives for agents to a 404.
174 *
175 * `/?author=N` is the classic user-enumeration probe: it resolves a
176 * numeric id to a `user_nicename`, which for an agent is the
177 * `agent-<slug>` login. Login is blocked regardless, so this is not
178 * exploitable on its own — but there is no reason to advertise the
179 * accounts, and an agent has no meaningful public archive.
180 *
181 * Only the front end is touched. The plugin's own REST surface and the
182 * wp-admin Users list still list agents normally.
183 *
184 * @param WP_Query $query The query about to run.
185 * @return void
186 */
187 function openstation_agent_block_author_archive( $query ) {
188 if ( is_admin() || ! $query->is_main_query() || ! $query->is_author() ) {
189 return;
190 }
191
192 $author_id = (int) $query->get( 'author' );
193 if ( $author_id <= 0 ) {
194 $name = $query->get( 'author_name' );
195 if ( is_string( $name ) && '' !== $name ) {
196 $user = get_user_by( 'slug', $name );
197 $author_id = $user ? (int) $user->ID : 0;
198 }
199 }
200
201 if ( $author_id > 0 && openstation_agent_is_agent( $author_id ) ) {
202 $query->set_404();
203 status_header( 404 );
204 nocache_headers();
205 }
206 }
207 add_action( 'pre_get_posts', 'openstation_agent_block_author_archive' );
208