3
0
mirror of https://github.com/snipe/snipe-it.git synced 2026-08-18 19:22:48 +00:00
Files
snipe-it/tests/Unit/Models/User/VerifyExactUsernameMatchTest.php

140 lines
4.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Unit\Models\User;
use App\Models\User;
use Tests\TestCase;
/**
* Coverage for User::verifyExactUsernameMatch, the SSO/federated-auth ATO
* guard against DB collation folding. The MySQL/MariaDB default collation
* utf8mb4_unicode_ci treats snipeitreport3 and snípeitreport3 as equal, so
* `WHERE username = ?` can return the wrong row for an attacker-controlled
* external identifier. The helper is the byte-exact re-check every federated
* auth callsite (SAML, LDAP, REMOTE_USER, Google OAuth) runs before trusting
* the resolved local user. These tests instantiate the User model without
* touching the DB because the helper is a pure function on the resolved row
* plus the expected string.
*
* Original vulnerability report: whale120 (@whale120_tw), DEVCORE Internship
* Program.
*/
class VerifyExactUsernameMatchTest extends TestCase
{
public function test_null_user_returns_null()
{
$this->assertNull(User::verifyExactUsernameMatch(null, 'anything'));
}
public function test_null_user_returns_null_even_when_expected_is_empty()
{
$this->assertNull(User::verifyExactUsernameMatch(null, ''));
}
public function test_exact_byte_match_returns_the_user()
{
$user = new User(['username' => 'snipeitreport3']);
$this->assertSame($user, User::verifyExactUsernameMatch($user, 'snipeitreport3'));
}
/**
* The literal customer-report scenario: MySQL's utf8mb4_unicode_ci returns
* the snipeitreport3 row when the query is snípeitreport3 (accented i).
* The helper must reject that row so SAML/LDAP/etc. cannot log in as it.
*/
public function test_accented_variant_is_rejected()
{
$user = new User(['username' => 'snipeitreport3']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'snípeitreport3'));
}
public function test_case_variant_is_rejected()
{
$user = new User(['username' => 'admin']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'Admin'));
}
public function test_uppercase_variant_is_rejected()
{
$user = new User(['username' => 'admin']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'ADMIN'));
}
/**
* Trailing/leading whitespace is a known family of collation-adjacent
* traps (PADSPACE semantics for CHAR columns, though users.username is
* VARCHAR); pin the behavior here so a future refactor can't drop into
* a trim()-based comparison and reintroduce a folding side channel.
*/
public function test_trailing_whitespace_variant_is_rejected()
{
$user = new User(['username' => 'snipeitreport3']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'snipeitreport3 '));
}
public function test_leading_whitespace_variant_is_rejected()
{
$user = new User(['username' => 'snipeitreport3']);
$this->assertNull(User::verifyExactUsernameMatch($user, ' snipeitreport3'));
}
public function test_different_length_variant_is_rejected()
{
$user = new User(['username' => 'admin']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'administrator'));
}
public function test_prefix_variant_is_rejected()
{
$user = new User(['username' => 'administrator']);
$this->assertNull(User::verifyExactUsernameMatch($user, 'admin'));
}
public function test_empty_expected_string_against_stored_username_is_rejected()
{
$user = new User(['username' => 'admin']);
$this->assertNull(User::verifyExactUsernameMatch($user, ''));
}
public function test_empty_stored_username_against_empty_expected_returns_the_user()
{
// Not a realistic identity but the helper must not throw on empty
// strings; hash_equals accepts them as long as both sides match.
$user = new User(['username' => '']);
$this->assertSame($user, User::verifyExactUsernameMatch($user, ''));
}
public function test_null_stored_username_is_rejected()
{
// users.username is nullable in the schema. Cast to string inside the
// helper coerces null to '' before comparing, so a stored null must
// never match any non-empty external identifier.
$user = new User(['username' => null]);
$this->assertNull(User::verifyExactUsernameMatch($user, 'anything'));
}
/**
* Cyrillic а (U+0430) is a common lookalike for Latin a (U+0061). Some
* collation configurations fold Cyrillic to Latin for confusable-detection
* purposes; the byte-exact guard must reject regardless.
*/
public function test_cyrillic_lookalike_variant_is_rejected()
{
$user = new User(['username' => 'admin']);
// First 'а' is Cyrillic U+0430, not Latin U+0061.
$this->assertNull(User::verifyExactUsernameMatch($user, 'аdmin'));
}
}