Skip to content
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
8 changes: 8 additions & 0 deletions backend/app/Helper/StringHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@

class StringHelper
{
/**
* Remove control characters and unicode line/paragraph separators from a plain-text value.
*/
public static function stripControlCharacters(string $text): string
{
return preg_replace('/[\x{0000}-\x{001F}\x{007F}-\x{009F}\x{2028}\x{2029}]/u', '', $text) ?? $text;
}

public static function previewFromHtml(string $text, int $length = 100): string
{
$textWithSpaces = preg_replace('/<[^>]+>/', ' ', $text);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use HiEvents\Events\EventUpdateEvent;
use HiEvents\Exceptions\CannotChangeCurrencyException;
use HiEvents\Helper\DateHelper;
use HiEvents\Helper\StringHelper;
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
use HiEvents\Repository\Interfaces\OrderRepositoryInterface;
use HiEvents\Services\Application\Handlers\Event\DTO\UpdateEventDTO;
Expand Down Expand Up @@ -71,7 +72,7 @@ private function updateEventAttributes(UpdateEventDTO $eventData): void

$this->eventRepository->updateWhere(
attributes: [
'title' => $eventData->title,
'title' => StringHelper::stripControlCharacters($eventData->title),
'category' => $eventData->category?->value ?? $existingEvent->getCategory(),
'start_date' => DateHelper::convertToUTC($eventData->start_date, $eventData->timezone),
'end_date' => $eventData->end_date
Expand Down
3 changes: 2 additions & 1 deletion backend/app/Services/Domain/Event/CreateEventService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use HiEvents\Exceptions\OrganizerNotFoundException;
use HiEvents\Helper\DateHelper;
use HiEvents\Helper\IdHelper;
use HiEvents\Helper\StringHelper;
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
use HiEvents\Repository\Interfaces\EventSettingsRepositoryInterface;
use HiEvents\Repository\Interfaces\EventStatisticRepositoryInterface;
Expand Down Expand Up @@ -94,7 +95,7 @@ private function getOrganizer(int $organizerId, int $accountId): OrganizerDomain
private function handleEventCreate(EventDomainObject $eventData): EventDomainObject
{
return $this->eventRepository->create([
'title' => $eventData->getTitle(),
'title' => StringHelper::stripControlCharacters($eventData->getTitle()),
'organizer_id' => $eventData->getOrganizerId(),
'start_date' => DateHelper::convertToUTC($eventData->getStartDate(), $eventData->getTimezone()),
'end_date' => $eventData->getEndDate()
Expand Down
3 changes: 2 additions & 1 deletion backend/app/Services/Domain/Event/DuplicateEventService.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use HiEvents\DomainObjects\Status\EventStatus;
use HiEvents\DomainObjects\TaxAndFeesDomainObject;
use HiEvents\DomainObjects\WebhookDomainObject;
use HiEvents\Helper\StringHelper;
use HiEvents\Repository\Eloquent\Value\Relationship;
use HiEvents\Repository\Interfaces\AffiliateRepositoryInterface;
use HiEvents\Repository\Interfaces\EventRepositoryInterface;
Expand Down Expand Up @@ -81,7 +82,7 @@ public function duplicateEvent(
$event = $this->getEventWithRelations($eventId, $accountId);

$event
->setTitle($title)
->setTitle(StringHelper::stripControlCharacters($title))
->setStartDate($startDate)
->setEndDate($endDate)
->setDescription($this->purifier->purify($description))
Expand Down
31 changes: 31 additions & 0 deletions backend/tests/Unit/Helper/StringHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Tests\Unit\Helper;

use HiEvents\Helper\StringHelper;
use Tests\TestCase;

class StringHelperTest extends TestCase
{
public function testStripControlCharactersRemovesControlAndSeparatorCharacters(): void
{
$input = "Hello\x00\x1F\x7F\u{2028}\u{2029}World";

$this->assertSame('HelloWorld', StringHelper::stripControlCharacters($input));
}

public function testStripControlCharactersPreservesLegitimatePrintableCharacters(): void
{
$input = 'Rock & Roll: a < b > c "Live" 🎸';

$this->assertSame($input, StringHelper::stripControlCharacters($input));
}

public function testStripControlCharactersPreservesScriptLikeText(): void
{
// The title is stored faithfully; escaping is the renderer's responsibility.
$input = '</script><svg onload=alert(1)>';

$this->assertSame($input, StringHelper::stripControlCharacters($input));
}
}
17 changes: 9 additions & 8 deletions frontend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as nodePath from "node:path";
import * as nodeUrl from "node:url";
import "dotenv/config";
import {sitemapIndexHandler, sitemapEventsHandler, sitemapOrganizersHandler} from "./src/sitemap/proxy.js";
import {htmlSafeJsonStringify} from "./src/utilites/safeScriptJson.js";

installGlobals();

Expand Down Expand Up @@ -59,7 +60,7 @@ async function main() {
envVars[key] = process.env[key];
}
}
return JSON.stringify(envVars);
return htmlSafeJsonStringify(envVars);
};

app.get('/robots.txt', (req, res) => {
Expand Down Expand Up @@ -98,7 +99,7 @@ Sitemap: ${frontendUrl}/sitemap.xml
{ req, res },
ssrManifest
);
const stringifiedState = JSON.stringify(dehydratedState);
const stringifiedState = htmlSafeJsonStringify(dehydratedState);

const helmetHtml = Object.values(helmetContext.helmet || {})
.map((value) => value.toString() || "")
Expand All @@ -114,11 +115,11 @@ Sitemap: ${frontendUrl}/sitemap.xml
}

const html = template
.replace("<!--head-snippets-->", headSnippets.join("\n"))
.replace("<!--app-html-->", appHtml)
.replace("<!--dehydrated-state-->", `<script>window.__REHYDRATED_STATE__ = ${stringifiedState}</script>`)
.replace("<!--environment-variables-->", envVariablesHtml)
.replace(/<!--render-helmet-->.*?<!--\/render-helmet-->/s, helmetHtml);
.replace("<!--head-snippets-->", () => headSnippets.join("\n"))
.replace("<!--app-html-->", () => appHtml)
.replace("<!--dehydrated-state-->", () => `<script>window.__REHYDRATED_STATE__ = ${stringifiedState}</script>`)
.replace("<!--environment-variables-->", () => envVariablesHtml)
.replace(/<!--render-helmet-->.*?<!--\/render-helmet-->/s, () => helmetHtml);

res.setHeader("Content-Type", "text/html");
return res.status(200).end(html);
Expand Down Expand Up @@ -147,4 +148,4 @@ Sitemap: ${frontendUrl}/sitemap.xml

}
}
main();
main();
3 changes: 2 additions & 1 deletion frontend/src/components/common/EventDocumentHead/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Helmet} from "react-helmet-async";
import {Event} from "../../../types";
import {eventCoverImageUrl, eventHomepageUrl} from "../../../utilites/urlHelper.ts";
import {utcToTz} from "../../../utilites/dates.ts";
import {htmlSafeJsonStringify} from "../../../utilites/safeScriptJson.js";

interface EventDocumentHeadProps {
event: Event;
Expand Down Expand Up @@ -90,7 +91,7 @@ export const EventDocumentHead = ({event}: EventDocumentHeadProps) => {
<link rel="canonical" href={url}/>

<script type="application/ld+json">
{JSON.stringify(schemaOrgJSONLD)}
{htmlSafeJsonStringify(schemaOrgJSONLD)}
</script>
</Helmet>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/utilites/safeScriptJson.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export declare const htmlSafeJsonStringify: (value: unknown) => string;
10 changes: 10 additions & 0 deletions frontend/src/utilites/safeScriptJson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Serialise a value to JSON that is safe to embed inside an inline <script> tag.
*/
export const htmlSafeJsonStringify = (value) =>
(JSON.stringify(value) ?? "null")
.replace(/</g, "\\u003c")
.replace(/>/g, "\\u003e")
.replace(/&/g, "\\u0026")
.replace(/\u2028/g, "\\u2028")
.replace(/\u2029/g, "\\u2029");
Loading