Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SVG test case visualisation for jury submission #2915

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions webapp/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.14",
"eligrey/filesaver": "2.*",
"enshrined/svg-sanitize": "^0.21.0",
"fortawesome/font-awesome": "6.*",
"friendsofsymfony/rest-bundle": "^3.5",
"ircmaxell/password-compat": "*",
Expand Down
47 changes: 46 additions & 1 deletion webapp/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 20 additions & 10 deletions webapp/src/Controller/Jury/ProblemController.php
Original file line number Diff line number Diff line change
Expand Up @@ -605,16 +605,26 @@ public function testcasesAction(Request $request, int $probId): Response
}
$content = file_get_contents($file->getRealPath());
if ($type === 'image') {
$imageType = Utils::getImageType($content, $error);
if ($imageType === false) {
$this->addFlash('danger', sprintf('image: %s', $error));
return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]);
}
$thumb = Utils::getImageThumb($content, $thumbnailSize,
$this->dj->getDomjudgeTmpDir(), $error);
if ($thumb === false) {
$this->addFlash('danger', sprintf('image: %s', $error));
return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]);
if (mime_content_type($file->getRealPath()) === 'image/svg+xml') {
$content = Utils::sanitizeSvg($content);
if ($content === false) {
$this->addFlash('danger', sprintf('image: %s', $error));
return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]);
}
$thumb = $content;
$imageType = 'svg';
} else {
$imageType = Utils::getImageType($content, $error);
if ($imageType === false) {
$this->addFlash('danger', sprintf('image: %s', $error));
return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]);
}
$thumb = Utils::getImageThumb($content, $thumbnailSize,
$this->dj->getDomjudgeTmpDir(), $error);
if ($thumb === false) {
$this->addFlash('danger', sprintf('image: %s', $error));
return $this->redirectToRoute('jury_problem_testcases', ['probId' => $probId]);
}
}

$testcase->setImageType($imageType);
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Jury/SubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ public function viewAction(
'requestedOutputCount' => $requestedOutputCount,
'version_warnings' => [],
'isMultiPassProblem' => $submission->getProblem()->isMultipassProblem(),
'thumbnailSize' => $this->config->get('thumbnail_size'),
];

if ($selectedJudging === null) {
Expand Down
1 change: 1 addition & 0 deletions webapp/src/Controller/Team/SubmissionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ public function viewAction(Request $request, int $submitId): Response
'showSampleOutput' => $showSampleOutput,
'runs' => $runs,
'showTooLateResult' => $showTooLateResult,
'thumbnailSize' => $this->config->get('thumbnail_size'),
];
if ($actuallyShowCompile) {
$data['size'] = 'xl';
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/Service/DOMJudgeService.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ class DOMJudgeService
'image/svg+xml' => 'svg',
];

final public const EXTENSION_TO_MIMETYPE = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'svg' => 'image/svg+xml',
];

public function __construct(
protected readonly EntityManagerInterface $em,
protected readonly LoggerInterface $logger,
Expand Down
9 changes: 9 additions & 0 deletions webapp/src/Service/ImportProblemService.php
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,15 @@ public function importZippedProblem(
break;
}
}
// Handle SVG differently, as a lot of the above concepts do not make sense in this context.
$imageFileName = $baseFileName . '.svg';
if (($imageFile = $zip->getFromName($imageFileName)) !== false) {
if (($imageFile = Utils::sanitizeSvg($imageFile)) === false) {
$messages['warning'][] = sprintf("Contents of '%s' is not safe.", $imageFileName);
}
$imageType = 'svg';
$imageThumb = $imageFile;
}

if (str_contains($testInput, "\r")) {
$messages['warning'][] = "Testcase file '$baseFileName.in' contains Windows newlines.";
Expand Down
6 changes: 6 additions & 0 deletions webapp/src/Twig/TwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public function getFilters(): array
new TwigFilter('entityIdBadge', $this->entityIdBadge(...), ['is_safe' => ['html']]),
new TwigFilter('medalType', $this->awards->medalType(...)),
new TwigFilter('numTableActions', $this->numTableActions(...)),
new TwigFilter('extensionToMime', $this->extensionToMime(...)),
];
}

Expand Down Expand Up @@ -1346,4 +1347,9 @@ protected function numTableActions(array $tableData): int
}
return $maxNumActions;
}

public function extensionToMime(string $extension): string
{
return DOMJudgeService::EXTENSION_TO_MIMETYPE[$extension];
}
}
10 changes: 10 additions & 0 deletions webapp/src/Utils/Utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

use DateTime;
use Doctrine\Inflector\InflectorFactory;
use enshrined\svgSanitize\Sanitizer as SvgSanitizer;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

Expand Down Expand Up @@ -717,6 +718,15 @@ public static function getImageSize(string $filename): array
return [$width, $height, $width / $height];
}

public static function sanitizeSvg(string $svgContents): string | false
{
$sanitizer = new SvgSanitizer();
$sanitizer->removeRemoteReferences(true);
$sanitizer->minify(true);

return $sanitizer->sanitize($svgContents);
}

/**
* Returns TRUE iff string $haystack starts with string $needle.
*/
Expand Down
7 changes: 6 additions & 1 deletion webapp/templates/jury/submission.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
.judging-table tr.disabled td a {
color: silver
}

.image_thumb {
max-width: {{ thumbnailSize }}px;
max-height: {{ thumbnailSize }}px;
}
</style>
{% endblock %}

Expand Down Expand Up @@ -742,7 +747,7 @@
<span style="float:right; border: 3px solid #438ec3; margin: 5px; padding: 5px;">
{% set imgUrl = path('jury_problem_testcase_fetch', {'probId': submission.problem.probid, 'rank': run.rank, 'type': 'image'}) %}
<a href="{{ imgUrl }}">
<img src="data:image/{{ run.imageType }};base64,{{ runsOutput[runIdx].image_thumb | base64 }}"/>
<img class="image_thumb" src="data:{{ run.imageType | extensionToMime }};base64,{{ runsOutput[runIdx].image_thumb | base64 }}"/>
</a>
</span>
{% endif %}
Expand Down
49 changes: 49 additions & 0 deletions webapp/tests/Unit/Utils/UtilsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,55 @@ public function provideTestGetImageSize(): Generator
yield [__DIR__ . '/../../../public/images/DOMjudgelogo.svg', 510, 1122];
}

public function testSanitizeSvg(): void
{
// SVG source: https://svg.enshrined.co.uk/
$dirty = <<<EOF
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<rect fill="url('http://example.com/benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<rect fill="url('https://example.com/benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<rect fill=" url( ' https://example.com/benis.svg ' ) " x="0" y="0" width="1000" height="1000"></rect>
<rect fill="url('ftp://192.168.2.1/benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<rect fill="url('//example.com/benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<rect fill="url('/benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<rect fill="url('#benis.svg')" x="0" y="0" width="1000" height="1000"></rect>
<g id="righteye" class="eye">
<path id="iris-2" data-name="iris" class="cls-4" d="M241.4,143.6s18.5,11.9,36,7.1,29.6-15.8,27.2-24.6c-1.7-6-9.8-9.4-20.3-9.4a59.21,59.21,0,0,0-15.6,2.2,37.44,37.44,0,0,0-12.4,6.4,60.14,60.14,0,0,0-14.9,18.3" transform="translate(-9.7 -9.3)"/>
<path id="lid" class="cls-11" d="M304.5,124.4c-1.7-6-9.8-9.4-20.3-9.4a59.21,59.21,0,0,0-15.6,2.2,37.44,37.44,0,0,0-12.4,6.4,61.21,61.21,0,0,0-14.9,18.1" transform="translate(-9.7 -9.3)"/>
<path id="pupil-2" data-name="pupil" class="cls-12" d="M256.7,126.1c2.5,9.2,11,14.8,18.9,12.6s12.3-11.4,9.8-20.6a16.59,16.59,0,0,0-1.2-3.1,59.21,59.21,0,0,0-15.6,2.2,37.44,37.44,0,0,0-12.4,6.4,9.23,9.23,0,0,0,.5,2.5" transform="translate(-9.7 -9.3)"/>
<path id="eyelash-2" data-name="eyelash" class="cls-13" d="M302.9,122.3c7.7,2.5,17-5,20.8-16.8M292,115.7c7.6,2.8,17.2-4.4,21.4-16M277,115.1c8.1-.3,14.3-10.5,13.9-22.8" transform="translate(-9.7 -9.3)"/>
<path id="reflection-2" data-name="reflection" class="cls-14" d="M271.1,127.1c0,3.6-2.6,6.5-5.8,6.5s-5.8-2.9-5.8-6.5,2.6-6.4,5.8-6.4,5.8,2.9,5.8,6.4" transform="translate(-9.7 -9.3)"/>
</g>
<a href="javascript:alert(2)">test 1</a>
<a xlink:href="javascript:alert(2)">test 2</a>
<a href="#test3">test 3</a>
<a xlink:href="#test">test 4</a>

<a href="data:data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 5</a>
<a xlink:href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' onload='alert(88)'%3E%3C/svg%3E">test 6</a>
<use xlink:href="defs.svg#icon-1"/>
<line onload="alert(2)" fill="none" stroke="#000000" stroke-miterlimit="10" x1="119" y1="84.5" x2="454" y2="84.5"/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="111.212" y1="102.852" x2="112.032" y2="476.623"/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="198.917" y1="510.229" x2="486.622" y2="501.213"/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="484.163" y1="442.196" x2="89.901" y2="60.229"/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="101.376" y1="478.262" x2="443.18" y2="75.803"/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="457.114" y1="126.623" x2="458.753" y2="363.508"/>
<this>shouldn't be here</this>
<script>alert(1);</script>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="541.54" y1="299.573" x2="543.179" y2="536.458"/>

</svg>
EOF;
$clean = Utils::sanitizeSvg($dirty);
self::assertFalse(str_contains($clean, "script"));
self::assertFalse(str_contains($clean, "alert"));
self::assertFalse(str_contains($clean, "shouldn't be here"));
self::assertFalse(str_contains($clean, "example.com"));
self::assertTrue(str_contains($clean, '</svg>'));
}

/**
* Test that the wrapUnquoted function returns the correct result
*/
Expand Down
Loading