Bug-ul
O platformă SaaS B2B multi-tenant ține facturile și datele clienților pentru multe companii, iar fiecare companie ar trebui să le vadă doar pe ale ei. Paginile de cont aveau în adresă ID-ul de tenant al clientului, care arată ca o graniță, dar serverul nu-l verifica. Găsea utilizatorul țintă doar după ID-ul lui, iar ID-urile erau numere mici, consecutive.
Cum se vede
Fiecare client își vedea utilizatorii și datele lui, așa că nimic nu părea în neregulă. Dar dacă schimbai ID-ul utilizatorului cu unu, pagina arăta un utilizator din altă companie, cu un formular de setare a parolei adresat e-mailului lui. Editorul de permisiuni avea aceeași problemă și afișa e-mailul și drepturile de acces ale acelui utilizator. Împreună, cele două pagini îi permiteau oricărui client logat să vadă toți utilizatorii platformei și să aleagă cui îi setează parola, inclusiv administratorilor altor companii.
403 · Acces interzis
- Utilizatorul nu face parte din compania ta.
Refuzat de server
Setează o parolă nouă
- Pentru: admin@company-b.example
- Companie: Company B
Afișat unui cont din company A
Cum l-am găsit
Într-un audit de securitate ne-am logat ca un client și am schimbat ID-ul pe fiecare pagină care primește unul. Am dovedit problema cu o singură înregistrare din altă companie, doar citind, și ne-am oprit acolo. N-am trimis niciun formular și n-am schimbat nicio parolă.
Paginile cu înregistrări erau limitate corect la compania fiecăruia, iar raportul o spune: raportăm și ce funcționează.
Reparația
Ce am recomandat: serverul verifică dacă utilizatorul țintă e din compania celui care face cererea și dacă acesta are voie să-l administreze. Schimbarea parolei cere parola curentă sau o nouă autentificare. ID-urile consecutive se înlocuiesc cu ID-uri imposibil de ghicit.
Testul care îl prinde
Te loghezi ca firma A și ceri fiecare pagină de administrare a conturilor cu ID-ul unui utilizator din firma B. Testul așteaptă 403 sau 404 și verifică faptul că e-mailul lui B nu apare în răspuns. Rulează în CI pe fiecare rută care primește un ID.
import { test, expect } from "@playwright/test";
import { accountRoutes, seed, signInAs } from "./fixtures";
// Every account page that takes a user ID, asked for another company's user.
for (const route of accountRoutes) {
test(`company A can't open a company B user: ${route}`, async ({ page }) => {
await signInAs(page, seed.companyA.admin);
const userB = seed.companyB.admin;
const response = await page.goto(route.replace(":userId", userB.id));
expect([403, 404]).toContain(response?.status());
await expect(page.getByText(userB.email)).toHaveCount(0);
});
}
Ce pune în pericol
Fiecare cont de pe platformă și, prin aceste conturi, facturile și datele clienților fiecărei companii. Conform GDPR, o astfel de preluare e o încălcare a securității datelor personale pentru fiecare companie afectată.
