fix: escape scenario id when serialising saved page
PostTeamEditor interpolated the scenario id into a <script> block as
'localStorage.setItem("scenario:<id>", ...)', so an id containing
'</script><script>...</script>' would break out of the JS-string context
and execute in the browser. Use json_encode with JSON_HEX_TAG / HEX_AMP /
HEX_APOS / HEX_QUOT so the id is always a safe JS string literal.
Adds a PostTeamEditorTest case that submits '</script><script>alert(1)</script>'
and asserts the literal '<script>alert(1)</script>' substring is absent
from the response. Updates the existing happy-path and FullFlowTest
assertions to match the new 'scenario:"demo"' (quoted) form.
This commit is contained in:
@@ -45,7 +45,7 @@ final class FullFlowTest extends TestCase
|
||||
$teamBody = $this->runFrontController();
|
||||
self::assertSame(200, $this->lastStatus);
|
||||
self::assertStringContainsString('localStorage.setItem', $teamBody);
|
||||
self::assertStringContainsString('scenario:demo', $teamBody);
|
||||
self::assertStringContainsString('scenario:"demo"', $teamBody);
|
||||
|
||||
// Step 3: POST battlefield editor.
|
||||
$_SERVER['REQUEST_METHOD'] = 'POST';
|
||||
|
||||
@@ -46,7 +46,7 @@ final class PostTeamEditorTest extends TestCase
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
self::assertStringContainsString('localStorage.setItem', $response->body);
|
||||
self::assertStringContainsString('scenario:demo', $response->body);
|
||||
self::assertStringContainsString('scenario:"demo"', $response->body);
|
||||
}
|
||||
|
||||
public function testItRejectsAnOutOfBoundsStatAndReRendersTheForm(): void
|
||||
@@ -67,6 +67,26 @@ final class PostTeamEditorTest extends TestCase
|
||||
self::assertStringContainsString('outside archetype bounds', $response->body);
|
||||
}
|
||||
|
||||
public function testItEscapesAMaliciousIdSoItCannotBreakOutOfTheScriptBlock(): void
|
||||
{
|
||||
[$token, $cookie] = CsrfToken::issue(self::SECRET);
|
||||
$handler = new PostTeamEditor();
|
||||
$post = $this->validPost($token);
|
||||
$post['id'] = '</script><script>alert(1)</script>';
|
||||
$request = $this->buildRequest(
|
||||
post: $post,
|
||||
cookies: ['__csrf' => $cookie],
|
||||
server: ['__csrf_secret' => self::SECRET],
|
||||
);
|
||||
$response = $handler->handle($request, ['id' => $post['id']]);
|
||||
|
||||
self::assertSame(200, $response->status);
|
||||
// The literal payload that would execute must NOT appear verbatim in the body.
|
||||
self::assertStringNotContainsString('<script>alert(1)</script>', $response->body);
|
||||
// The JSON-encoded id is what we want to see, with the </ tag escaped to <\/.
|
||||
self::assertStringContainsString('<\\/script><script>alert(1)<\\/script>', $response->body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $post
|
||||
* @param array<string, string> $cookies
|
||||
|
||||
Reference in New Issue
Block a user