Skip to content

Commit a731ea7

Browse files
committed
Match asset files on their extension instead of a substring
The custom CSS/JS and image lookups matched any filename containing ".css", ".js", ".png" and so on, so backup files like "theme.css.bak" or a stray "data.json" were served as stylesheets or scripts. Every directory was also checked against all extensions, so an image in js/custom/ would have been included as a script. Match on the filename suffix and let each caller pass the extensions that are valid for its directory.
1 parent bf65b3d commit a731ea7

4 files changed

Lines changed: 65 additions & 11 deletions

File tree

webapp/src/Service/DOMJudgeService.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,31 +1425,34 @@ public function apiRelativeUrl(string $route, array $params = []): string
14251425
}
14261426

14271427
/**
1428-
* Get asset files in the given directory with the given extension
1428+
* Get asset files in the given directory ending in one of the given extensions
14291429
*
1430+
* @param string[] $extensions
14301431
* @return string[]
14311432
*/
1432-
public function getAssetFiles(string $path): array
1433+
public function getAssetFiles(string $path, array $extensions): array
14331434
{
1434-
if (isset($this->assetFilesCache[$path])) {
1435-
return $this->assetFilesCache[$path];
1435+
$cacheKey = $path . '|' . implode(',', $extensions);
1436+
if (isset($this->assetFilesCache[$cacheKey])) {
1437+
return $this->assetFilesCache[$cacheKey];
14361438
}
14371439

14381440
$customDir = sprintf('%s/public/%s', $this->params->get('kernel.project_dir'), $path);
14391441
if (!is_dir($customDir)) {
1440-
return $this->assetFilesCache[$path] = [];
1442+
return $this->assetFilesCache[$cacheKey] = [];
14411443
}
14421444

14431445
$results = [];
14441446
foreach (scandir($customDir) as $file) {
1445-
foreach (array_merge(['css','js'], static::MIMETYPE_TO_EXTENSION) as $extension) {
1446-
if (str_contains($file, '.' . $extension)) {
1447+
foreach ($extensions as $extension) {
1448+
if (str_ends_with($file, '.' . $extension)) {
14471449
$results[] = $file;
1450+
break;
14481451
}
14491452
}
14501453
}
14511454

1452-
return $this->assetFilesCache[$path] = $results;
1455+
return $this->assetFilesCache[$cacheKey] = $results;
14531456
}
14541457

14551458
/**
@@ -1475,7 +1478,7 @@ public function assetPath(?string $name, string $type, bool $fullPath = false, ?
14751478
}
14761479

14771480
if (isset($dir)) {
1478-
$assets = $this->getAssetFiles($dir);
1481+
$assets = $this->getAssetFiles($dir, array_values(static::MIMETYPE_TO_EXTENSION));
14791482
foreach (static::MIMETYPE_TO_EXTENSION as $extension) {
14801483
if ($forceExtension === $extension || (!$forceExtension && in_array($name . '.' . $extension, $assets))) {
14811484
return sprintf('%s%s/%s.%s', $prefix, $dir, $name, $extension);

webapp/src/Twig/TwigExtension.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,7 @@ public function printContestStart(Contest $contest): string
988988
public function customAssetFiles(string $type): array
989989
{
990990
if (in_array($type, ['css', 'js'])) {
991-
return $this->dj->getAssetFiles("$type/custom");
991+
return $this->dj->getAssetFiles("$type/custom", [$type]);
992992
}
993993

994994
return [];
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace App\Tests\Unit\Service;
4+
5+
use App\Service\DOMJudgeService;
6+
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
7+
use Symfony\Component\Filesystem\Filesystem;
8+
9+
class DOMJudgeServiceTest extends KernelTestCase
10+
{
11+
private const ASSET_PATH = 'test-assets';
12+
13+
private DOMJudgeService $dj;
14+
private string $assetDir;
15+
16+
protected function setUp(): void
17+
{
18+
self::bootKernel();
19+
$this->dj = static::getContainer()->get(DOMJudgeService::class);
20+
$this->assetDir = static::getContainer()->getParameter('kernel.project_dir') . '/public/' . self::ASSET_PATH;
21+
(new Filesystem())->mkdir($this->assetDir);
22+
}
23+
24+
protected function tearDown(): void
25+
{
26+
(new Filesystem())->remove($this->assetDir);
27+
parent::tearDown();
28+
}
29+
30+
public function testGetAssetFilesMatchesOnTheExtensionOnly(): void
31+
{
32+
foreach (['ok.css', 'also-ok.css', 'backup.css.bak', 'data.json', 'script.js', 'image.png'] as $file) {
33+
touch($this->assetDir . '/' . $file);
34+
}
35+
36+
self::assertSame(['also-ok.css', 'ok.css'], $this->dj->getAssetFiles(self::ASSET_PATH, ['css']));
37+
self::assertSame(['image.png', 'script.js'], $this->dj->getAssetFiles(self::ASSET_PATH, ['png', 'js']));
38+
}
39+
}

webapp/tests/Unit/Twig/TwigExtensionTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,18 @@ class TwigExtensionTest extends TestCase
4747
private const XSS_PAYLOAD = '<img src=x onerror="alert(1)">';
4848

4949
private TwigExtension $twigExtension;
50+
private DOMJudgeService&MockObject $dj;
5051
private RouterInterface&MockObject $router;
5152
private SerializerInterface&MockObject $serializer;
5253

5354
protected function setUp(): void
5455
{
56+
$this->dj = $this->createMock(DOMJudgeService::class);
5557
$this->router = $this->createMock(RouterInterface::class);
5658
$this->serializer = $this->createMock(SerializerInterface::class);
5759

5860
$this->twigExtension = new TwigExtension(
59-
$this->createMock(DOMJudgeService::class),
61+
$this->dj,
6062
$this->createMock(ConfigurationService::class),
6163
$this->createMock(Environment::class),
6264
$this->createMock(EntityManagerInterface::class),
@@ -73,6 +75,16 @@ protected function setUp(): void
7375

7476

7577

78+
public function testCustomAssetFilesOnlyAsksForFilesOfItsOwnType(): void
79+
{
80+
$this->dj->expects(self::once())
81+
->method('getAssetFiles')
82+
->with('js/custom', ['js'])
83+
->willReturn(['a.js', 'b.js']);
84+
85+
self::assertSame(['a.js', 'b.js'], $this->twigExtension->customAssetFiles('js'));
86+
}
87+
7688
public function testPrintMetadataNull(): void
7789
{
7890
self::assertSame('', $this->twigExtension->printMetadata(null));

0 commit comments

Comments
 (0)