Let administrators link Microsoft accounts whose UPN differs from mail
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

Privileged accounts are never linked through the settable mail
attribute. Two new ways make that workable when UPN and e-mail differ:

- "Link Microsoft account" on the profile screen: the signed-in user
  (nonce, same browser via the state cookie, same user at the callback)
  signs in with Microsoft once and binds that identity. Existing links
  can only be removed by an administrator; an object ID bound elsewhere
  is refused.
- "Assigned Microsoft account (UPN)" per user, editable by
  administrators, used by sign-in and user sync; with an option to
  remove a link.

Sign-in now finds accounts by bound object ID first, then by assigned
UPN, then by e-mail, so linked users sign in whatever their addresses.

Also: third-audit report (docs/security-audit.md section 7), README
section on linking administrator accounts, translations, tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-24 03:57:45 +00:00
parent 9b893e42bc
commit 81b3a74ae5
12 changed files with 1583 additions and 995 deletions

View file

@ -1,6 +1,6 @@
# Security-Audit: M365 Login 1.1.0
**Stand:** 23.09.2026 (Erst-Audit 22.09.2026, vollständiges Zweit-Audit 23.09.2026) · **Umfang:** gesamter Plugin-Code
**Stand:** 24.09.2026 (Erst-Audit 22.09.2026, Zweit-Audit 23.09.2026, Dritt-Audit 24.09.2026) · **Umfang:** gesamter Plugin-Code
(PHP, JS, CSS) inkl. Benutzer-Sync, Konfiguration, Deployment-Hinweise ·
**Methode:** Code-Review gegen OAuth 2.0 / OpenID Connect Best Current Practice (RFC 6749, RFC 7636 PKCE,
RFC 7523 Client Assertions, OAuth 2.0 Security BCP), OWASP ASVS 4.0 (V2 Authentication, V3 Session, V5 Validation,
@ -16,15 +16,16 @@ echte HTTP-Requests (PHP-Webserver + curl) gegen `wp-login.php`, `xmlrpc.php` un
## 1. Zusammenfassung
| Schweregrad | Erst-Audit | Zweit-Audit | Behoben | Akzeptiert / dokumentiert |
| --- | --- | --- | --- | --- |
| Kritisch | 0 | 1 (nur Multisite) | 1 | 0 |
| Hoch | 1 | 4 | 5 | 0 |
| Mittel | 3 | 9 | 12 | 0 |
| Niedrig | 5 | 11 | 12 | 4 |
| Hinweis | 6 | 11 | 4 | 13 |
| Schweregrad | Erst-Audit | Zweit-Audit | Dritt-Audit | Behoben | Akzeptiert / dokumentiert |
| --- | --- | --- | --- | --- | --- |
| Kritisch | 0 | 1 (nur Multisite) | 0 | 1 | 0 |
| Hoch | 1 | 4 | 3 | 8 | 0 |
| Mittel | 3 | 9 | 5 | 17 | 0 |
| Niedrig | 5 | 11 | 9 | 20 | 5 |
| Hinweis | 6 | 11 | 8 | 8 | 17 |
Nach beiden Audits sind **keine offenen kritischen, hohen oder mittleren Befunde** bekannt. Die schwersten Funde des
Nach drei Audits sind **keine offenen kritischen, hohen oder mittleren Befunde** bekannt. Das Dritt-Audit hat gezielt die
Fixes des Zweit-Audits angegriffen und dabei unter anderem eine Umgehung über XML-RPC `system.multicall` gefunden. Die schwersten Funde des
Zweit-Audits betrafen nicht den OIDC-Kern (der hielt allen Angriffen stand), sondern die Ränder: Umgehung des
Nur-Button-Modus über `xmlrpc.php`/REST, die Verknüpfung von Administrator-Konten über das frei setzbare
`mail`-Attribut und Multisite-Rechte.
@ -256,7 +257,46 @@ Status: ✅ behoben (mit Regressionstest) · 📄 akzeptiert/dokumentiert
- **Deaktivierte Konten:** Passwort, XML-RPC (Passwort und Application Password), REST, bestehende Cookies, Microsoft-Callback, Super-Admin alle abgewiesen; keine Selbst-Reaktivierung möglich.
- **Graph-Client:** Paging-Links auf `https://graph.microsoft.com/v1.0/` festgelegt, 1000-Seiten-Limit, jeder Fehler bricht vor Änderungen ab.
## 7. Nicht im Umfang
## 7. Dritt-Audit 1.1.0 (24.09.2026)
Drei unabhängige Prüfbereiche: (1) Review aller Fixes des Zweit-Audits auf Vollständigkeit, Umgehbarkeit und neue Fehler,
(2) frischer Penetrationstest der Anmeldewege, (3) Sync, Admin-Oberfläche, Datenhaltung und Datenschutz. Jeder Befund per
Proof of Concept bestätigt (HTTP gegen `xmlrpc.php`/`wp-login.php`, signierte Test-Tokens, simulierte Graph-API, in eine
Multisite umgewandelte Testinstanz); jeder Fix mit Regressionstest.
| ID | Schwere | Befund | Status und Fix |
| --- | --- | --- | --- |
| D-1 | Hoch | **Nur-Button-Modus über XML-RPC `system.multicall`:** Die Ausnahme für Application Passwords nutzte `did_action()` (anfrageweit). Aufruf 1 mit einem beliebigen eigenen Application Password, Aufruf 2 mit Admin-Name und normalem Passwort → Admin-Zugriff. | ✅ Ausnahme nur für genau den Benutzer, den das Application Password im selben Anmeldedurchlauf authentifiziert hat (Reset bei Priorität 0). Per HTTP nachgetestet. |
| D-2 | Hoch | Admin-Schutzregel griff nicht bei ausgeschalteter Objekt-ID-Bindung (`bind_oid`) bzw. bei bereits gespeicherter, aber nicht geprüfter ID. | ✅ Privilegierte Konten gelten nur bei aktiver Bindung und passender ID als verknüpft, sonst gilt immer die Regel. |
| D-3 | Hoch (Multisite) | „Privilegiert“ wurde nur auf der aktuellen Site geprüft: Admin von Site B über Site A übernehmbar. | ✅ Rechte auf allen Sites des Benutzers zählen. |
| D-4 | Mittel | Liste privilegierter Rechte zu eng (Redakteure mit `unfiltered_html`, Plugin-/Theme-/Benutzerrechte fehlten). | ✅ Erweitert; Filter `m365_login_is_privileged_user`. |
| D-5 | Mittel | Deaktivierte Admins (ohne Rolle) galten als nicht privilegiert → verknüpfbar, bei Reaktivierung wieder Admin. | ✅ Gemerkte Rollen deaktivierter Konten zählen mit. |
| D-6 | Mittel | Cookie-Sperre (Z-3/Z-6) wirkungslos unter WordPress 6.0/6.1 (Filter-Argumente erst ab 6.2). | ✅ Ohne Benutzer-ID-Argument wird in API-Kontexten immer gesperrt. |
| D-7 | Mittel | Sicherheitsstopp durch im selben Lauf angelegte Konten verwässert; Testlauf und echter Lauf entschieden unterschiedlich. | ✅ Quote aus den vor dem Lauf verknüpften Konten. |
| D-8 | Mittel | Rollen-Entzug ohne Sicherheitsstopp: eine geleerte Gruppe stufte alle zugeordneten Admins herab. | ✅ Eigener Stopp für den Entzug administrativer Rollen (max. 20 %, nie alle; Filter `m365_login_sync_demotion_limit`). |
| D-9 | Niedrig | Lauf-Sperre per `add_option` nicht atomar, lief bei langen Läufen ab. | ✅ `INSERT IGNORE`, Übernahme per bedingtem `UPDATE`, Auffrischung alle 250 Benutzer. |
| D-10 | Niedrig | Tenant-Schutz verzögerte nur um einen Lauf und griff beim ersten Sync nicht. | ✅ Tenant wird pro Konto gespeichert; deprovisioniert wird nur im eigenen Tenant; Alt-Verknüpfungen ohne Tenant, die nicht gefunden werden, bleiben unangetastet (Warnung). |
| D-11 | Niedrig | Doppeltes Deaktivieren überschrieb gemerkte Rollen; Zeilenaktions-Nonce nicht an den Zustand gebunden. | ✅ Deaktivieren idempotent; Nonce pro Zustand. |
| D-12 | Niedrig | Nur-Button-Modus mit kaputter Verbindung zeigte Passwortfelder, die nichts bewirkten. | ✅ Felder ausgeblendet, Hinweis „vorübergehend nicht verfügbar“. |
| D-13 | Niedrig | Altdaten aus 1.0: deaktivierte Konten nicht gehärtet, gespeichertes Key+Cert-Bündel. | ✅ Einmalige Migration; `.cer`-Download immer neu exportiert. |
| D-14 | Niedrig | Speicher bei sehr großen Tenants; ein Fatal Error hinterließ Sperre und keinen Bericht. | ✅ Laufzeit-Cache wird regelmäßig geleert, nur IDs gehalten; Shutdown-Handler meldet den Abbruch und gibt die Sperre frei. 📄 Für >20 000 Benutzer WP-CLI empfohlen. |
| D-15 | Niedrig | Profilbilder nur am Header geprüft (Polyglot, Dekompressionsbombe, EXIF). | ✅ Neu kodiert (240 px JPEG/PNG), max. 4096 px, Download-Limit 2 MB, `index.php` im Ordner, Löschung bei Deaktivierung. |
| D-16 | Niedrig | Keine Export-/Lösch-Werkzeuge (DSGVO). | ✅ Exporter und Eraser registriert. |
| D-17 | Niedrig | Rechte-Proxy mit mehreren Stufen (CDN → Load Balancer): rechter XFF-Eintrag ist der innere Proxy. | 📄 Einwertige Header wie `HTTP_CF_CONNECTING_IP` verwenden (Doku). |
| D-18 | Hinweis | Nicht-privilegiertes Konto, später befördert, bleibt mit seiner Verknüpfung. | 📄 Vertrauensmodell: Verknüpfung prüft beim Verknüpfen. |
| D-19 | Hinweis | Graph-App-Token 50 Min. im Klartext-Transient. | 📄 Wie Core-Transients; Datenbank schützen. |
| D-20 | Hinweis | Avatar-Auflösung per E-Mail-Adresse. | 📄 Bewusst (Kompatibilität mit `get_avatar( $email )`). |
| D-21 | Hinweis | Benutzernamen aus dem lokalen Teil der E-Mail, Anzeigenamen aus Graph. | 📄 Nur kosmetisch. |
| D-22 | Hinweis | Admins mit UPN ≠ E-Mail waren gar nicht mehr verknüpfbar. | ✅ Neu: „Mit Microsoft-Konto verknüpfen“ im Profil und zugewiesener UPN pro Benutzer; Anmeldung findet gebundene Konten über die Objekt-ID. |
| D-23…25 | Hinweis | Kleinere Robustheitspunkte (Notices bei Array-Eingaben, Autoload der Einstellungen bei Netzwerk-Aktivierung, `fields => array('ID')` durch Abfrage-Cache als String geliefert). | ✅ Behoben. |
**Neue Funktion aus Sicherheitssicht (D-22):** Die Profil-Verknüpfung verlangt eine gültige WordPress-Session, einen
Nonce, denselben Browser (State-Cookie) und dieselbe angemeldete Person beim Callback; eine bestehende Verknüpfung kann
nur ein Administrator aufheben, eine bereits anderweitig gebundene Objekt-ID wird abgelehnt. Restrisiko: Wer eine gültige
WordPress-Session eines noch nicht verknüpften Kontos stiehlt, kann dieses mit seinem Microsoft-Konto verknüpfen wie
bei jeder Selbstverwaltung eines zweiten Faktors.
## 8. Nicht im Umfang
Sicherheit der Microsoft-Seite (Entra ID, Graph), WordPress-Core, Hosting-Umgebung, andere Plugins/Themes,
Schwachstellen in PHP/OpenSSL.