mirror of https://github.com/pixelfed/pixelfed
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
5.7 KiB
PHP
168 lines
5.7 KiB
PHP
<?php
|
|
|
|
use App\Models\Profile;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
|
use Laravel\Passport\Passport;
|
|
|
|
uses(LazilyRefreshDatabase::class);
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Display name must not be HTML-entity encoded
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| Purify::clean entity-encodes &, <, > even in plain text, and strip_tags
|
|
| doesn't decode, so names like "Tom & Jerry" were stored as
|
|
| "Tom & Jerry" and became un-self-correctable. The pipeline must decode
|
|
| entities after stripping tags, while still removing XSS.
|
|
|
|
|
*/
|
|
|
|
it('preserves ampersands in the display name instead of entity-encoding', function () {
|
|
$user = User::factory()->create();
|
|
$user->refresh();
|
|
|
|
Passport::actingAs($user, ['write']);
|
|
|
|
$this->patchJson('/api/v1/accounts/update_credentials', [
|
|
'display_name' => 'Tom & Jerry',
|
|
])->assertOk();
|
|
|
|
$user->refresh();
|
|
expect($user->name)->toBe('Tom & Jerry');
|
|
expect(Profile::find($user->profile_id)->name)->toBe('Tom & Jerry');
|
|
});
|
|
|
|
it('strips tags but keeps surrounding text and ampersands', function () {
|
|
$user = User::factory()->create();
|
|
$user->refresh();
|
|
|
|
Passport::actingAs($user, ['write']);
|
|
|
|
$this->patchJson('/api/v1/accounts/update_credentials', [
|
|
'display_name' => 'Tom & <b>Jerry</b>',
|
|
])->assertOk();
|
|
|
|
$user->refresh();
|
|
expect($user->name)->toBe('Tom & Jerry');
|
|
});
|
|
|
|
it('removes script content (XSS protection preserved)', function () {
|
|
$user = User::factory()->create();
|
|
$user->refresh();
|
|
|
|
Passport::actingAs($user, ['write']);
|
|
|
|
$this->patchJson('/api/v1/accounts/update_credentials', [
|
|
'display_name' => '<script>alert(1)</script>Safe',
|
|
])->assertOk();
|
|
|
|
$user->refresh();
|
|
expect($user->name)->not->toContain('<script>');
|
|
expect($user->name)->not->toContain('alert(1)');
|
|
expect($user->name)->toContain('Safe');
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Bulk display-name corpus
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| ~50 names exercising the sanitize pipeline end-to-end through
|
|
| PATCH /api/v1/accounts/update_credentials. Every input is <= 30 raw chars
|
|
| so it passes the display_name max:30 validation and reaches the sanitizer.
|
|
| Expected outputs were computed against the live HTMLPurifier config.
|
|
|
|
|
*/
|
|
|
|
function updateDisplayName(string $name): string
|
|
{
|
|
$user = User::factory()->create();
|
|
$user->refresh();
|
|
|
|
Passport::actingAs($user, ['write']);
|
|
|
|
test()->patchJson('/api/v1/accounts/update_credentials', [
|
|
'display_name' => $name,
|
|
])->assertOk();
|
|
|
|
return (string) $user->fresh()->name;
|
|
}
|
|
|
|
// Legitimate names: must round-trip unchanged (no entity encoding, no loss).
|
|
it('preserves legitimate display names verbatim', function (string $name) {
|
|
expect(updateDisplayName($name))->toBe($name);
|
|
})->with([
|
|
'plain ascii' => ['Alice'],
|
|
'two words' => ['Bob Smith'],
|
|
'ampersand pair' => ['Tom & Jerry'],
|
|
'ampersand pair 2' => ['Ben & Jerry'],
|
|
'rock and roll' => ['Rock & Roll'],
|
|
'brand AT&T' => ['AT&T'],
|
|
'r and d' => ['R&D Team'],
|
|
'brand P&G' => ['P&G'],
|
|
'brand M&Ms' => ['M&Ms'],
|
|
'multi ampersand' => ['a & b & c'],
|
|
'diacritic zoe' => ['Zoë Müller'],
|
|
'diacritic jose' => ['José'],
|
|
'diacritic francois' => ['François'],
|
|
'diacritic renee' => ['Renée'],
|
|
'accented ampersand' => ['café & thé'],
|
|
'nordic soren' => ['Søren'],
|
|
'naive' => ['naïve'],
|
|
'cjk chinese' => ['北京老王'],
|
|
'cjk japanese' => ['山田太郎'],
|
|
'arabic' => ['علي'],
|
|
'cyrillic' => ['Даша'],
|
|
'greek' => ['Σωκράτης'],
|
|
'korean' => ['김민준'],
|
|
'emoji flower' => ['🌸 flower'],
|
|
'emoji cat' => ['cat 🐈 lover'],
|
|
'emoji inline' => ['emoji😀name'],
|
|
'cpp dev' => ['C++ Dev'],
|
|
'gt symbol' => ['5 > 3'],
|
|
'lt symbol' => ['2 < 4'],
|
|
'percent' => ['100% cotton'],
|
|
'dotted' => ['plain.name'],
|
|
'underscore' => ['under_score'],
|
|
'dashed' => ['dash-name'],
|
|
'quoted' => ['"Quoted"'],
|
|
'apostrophe' => ["It's me"],
|
|
'sql-looking but plain' => ["'; DROP TABLE u;--"],
|
|
]);
|
|
|
|
// Dirty-but-recoverable: tags stripped, entities decoded, exact readable output.
|
|
it('sanitizes dirty display names to the expected readable text', function (string $name, string $expected) {
|
|
expect(updateDisplayName($name))->toBe($expected);
|
|
})->with([
|
|
'bold tag stripped' => ['<b>Bold</b>', 'Bold'],
|
|
'bold around name' => ['Tom & <b>J</b>', 'Tom & J'],
|
|
'italic prefix' => ['<i>x</i>Safe', 'xSafe'],
|
|
'tag mid-string' => ['a<b>c', 'ac'],
|
|
'script stripped keeps tail' => ['<script>x</script>Y', 'Y'],
|
|
'style stripped keeps tail' => ['<style>q</style>W', 'W'],
|
|
'div handler stripped' => ['<div onclick=x>d</div>', 'd'],
|
|
'pre-encoded lt gt decoded' => ['<script>', '<script>'],
|
|
'pre-encoded amp decoded' => ['&pre', '&pre'],
|
|
'nested tags' => ['<b><i>hi</i></b>', 'hi'],
|
|
'unknown tag stripped' => ['<xyz>tag', 'tag'],
|
|
]);
|
|
|
|
// Dangerous inputs: whatever survives must carry no executable markup.
|
|
it('never stores executable markup', function (string $name) {
|
|
$stored = strtolower(updateDisplayName($name));
|
|
|
|
foreach (['<script', '</script', 'onerror', 'onload', 'onclick', '<img', '<svg', '<iframe', 'javascript:'] as $marker) {
|
|
expect($stored)->not->toContain($marker);
|
|
}
|
|
})->with([
|
|
'script tag' => ['<script>x</script>Y'],
|
|
'img onerror' => ['<img src=x onerror=y>'],
|
|
'svg onload' => ['<svg onload=z>'],
|
|
'div onclick' => ['<div onclick=x>d</div>'],
|
|
'style tag' => ['<style>q</style>W'],
|
|
'nested markup' => ['<b><i>hi</i></b>'],
|
|
'stray open tag' => [' <b> '],
|
|
]);
|