Fix the findings of a full second security audit
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run

Four-part audit (OIDC/JWT/crypto, user sync, admin UI, login bypasses)
with dynamic PoCs against a real WordPress install; every fix is covered
by a regression test. Report: docs/security-audit.md, section 6.

Critical/High
- Multisite: settings, AJAX actions and certificate download require
  manage_network_options (site admins could sign in as super admin).
- Privileged accounts are only linked (sync and first sign-in) via a
  matching UPN of a member account, never via the settable mail
  attribute; the sync never changes their e-mail address; e-mail change
  notifications stay on.
- Button-only mode exempts by credential (application passwords, WP-CLI)
  instead of request context, closing bypasses through xmlrpc.php and
  REST login handlers; API requests never receive login cookies.
- Multi-tenant mode refuses guest/external identities.

Medium/Low
- Same message for right and wrong passwords; button-only no longer
  switches off when the connection breaks; server-side fallback cookie
  expiry; correct fallback key beats IP lockouts; right-most proxy hop;
  higher start limit; one object ID per account.
- Deactivation sets a random password, revokes application passwords and
  removes the role (restored on reactivation); disabled people are
  deactivated even when their mail vanished; duplicate bindings handled.
- Sync: abort on empty directory answer, no deprovisioning right after a
  tenant change, atomic run lock, strict photo path validation.
- Certificates: key bundles refused, clean re-exported certificate.
- Array-safe sanitising, encoded redirect_to, per-action nonces, escaped
  role lists, no Graph sleeps during sign-in, warnings for public groups,
  multi-tenant group rules and missing salts, uninstall clears the token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 17:10:30 +00:00
parent 791f43a80b
commit 850f0dcd54
18 changed files with 1908 additions and 1270 deletions

View file

@ -3,6 +3,7 @@
'use strict';
var cfg = window.m365LoginAdmin || {};
cfg.nonces = cfg.nonces || {};
var i18n = cfg.i18n || {};
$( function () {
@ -166,7 +167,7 @@
$btn.prop( 'disabled', true );
$out.removeClass( 'is-success is-error' ).prop( 'hidden', false ).text( i18n.generating || '…' );
$.post( cfg.ajaxUrl, { action: cfg.certAction, nonce: cfg.nonce, op: 'generate' } ).done( function ( res ) {
$.post( cfg.ajaxUrl, { action: cfg.certAction, nonce: cfg.nonces.cert, op: 'generate' } ).done( function ( res ) {
if ( res && res.success ) {
$out.addClass( 'is-success' ).text( res.data.message );
window.setTimeout( function () { window.location.reload(); }, 800 );
@ -312,7 +313,7 @@
$.post( cfg.ajaxUrl, {
action: cfg.groupAction,
nonce: cfg.nonce,
nonce: cfg.nonces.groups,
query: query
} ).done( function ( res ) {
if ( ! res || ! res.success ) {
@ -369,7 +370,7 @@
$( '.m365-sync-run' ).prop( 'disabled', true );
$report.html( $( '<p class="m365-inline-result">' ).text( i18n.syncRunning ) );
$.post( cfg.ajaxUrl, { action: cfg.syncAction, nonce: cfg.nonce, op: op } ).done( function ( res ) {
$.post( cfg.ajaxUrl, { action: cfg.syncAction, nonce: cfg.nonces.sync, op: op } ).done( function ( res ) {
if ( res && res.success ) {
$report.html( res.data.html );
} else {
@ -400,7 +401,7 @@
$.post( cfg.ajaxUrl, {
action: cfg.action,
nonce: cfg.nonce,
nonce: cfg.nonces.test,
tenant: $( '#m365-tenant' ).val()
} ).done( function ( res ) {
if ( res && res.success ) {