Skip to content

Commit 96e7726

Browse files
authored
Add UTF-8 body content validation (#89)
The content validation has to run before any other validation to reject non-UTF-8 early enough. `preg_match()`'s `u` flag is used instead of `mb_check_encoding()` to avoid adding the `mbstring` extension as a dependency.
2 parents 218fe0b + 115a4c7 commit 96e7726

3 files changed

Lines changed: 64 additions & 4 deletions

File tree

src/Parser/SecurityTxtParser.php

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace Spaze\SecurityTxt\Parser;
55

6+
use LogicException;
67
use Spaze\SecurityTxt\Exceptions\SecurityTxtError;
78
use Spaze\SecurityTxt\Exceptions\SecurityTxtWarning;
89
use Spaze\SecurityTxt\Fetcher\SecurityTxtFetchResult;
@@ -28,7 +29,9 @@
2829
use Spaze\SecurityTxt\SecurityTxt;
2930
use Spaze\SecurityTxt\SecurityTxtValidationLevel;
3031
use Spaze\SecurityTxt\Signature\SecurityTxtSignature;
32+
use Spaze\SecurityTxt\Validator\SecurityTxtValidateResult;
3133
use Spaze\SecurityTxt\Validator\SecurityTxtValidator;
34+
use Spaze\SecurityTxt\Violations\SecurityTxtContentNotUtf8;
3235
use Spaze\SecurityTxt\Violations\SecurityTxtLineNoEol;
3336
use Spaze\SecurityTxt\Violations\SecurityTxtPossibelFieldTypo;
3437
use Spaze\SecurityTxt\Violations\SecurityTxtSpecViolation;
@@ -126,17 +129,32 @@ public function parseString(string $contents, ?string $fileLocation = null, ?int
126129
$this->expiresWarningThreshold = $expiresWarningThreshold;
127130
$this->initFieldProcessors();
128131
$this->lineErrors = $this->lineWarnings = [];
132+
$securityTxt = new SecurityTxt(SecurityTxtValidationLevel::AllowInvalidValues);
133+
if ($fileLocation !== null) {
134+
$securityTxt->setFileLocation($fileLocation);
135+
}
136+
if (@preg_match('//u', $contents) === false) { // Intentionally silenced
137+
$pregError = preg_last_error();
138+
if ($pregError !== PREG_BAD_UTF8_ERROR) {
139+
throw new LogicException('preg_match() failed with PCRE error code ' . $pregError);
140+
}
141+
return new SecurityTxtParseStringResult(
142+
$securityTxt,
143+
false,
144+
$strictMode,
145+
$this->expiresWarningThreshold,
146+
$this->lineErrors,
147+
$this->lineWarnings,
148+
new SecurityTxtValidateResult([new SecurityTxtContentNotUtf8()], []),
149+
);
150+
}
129151
$lines = $this->splitLines->splitLines($contents);
130152
$securityTxtFields = array_combine(
131153
array_map(function (SecurityTxtField $securityTxtField): string {
132154
return strtolower($securityTxtField->value);
133155
}, SecurityTxtField::cases()),
134156
SecurityTxtField::cases(),
135157
);
136-
$securityTxt = new SecurityTxt(SecurityTxtValidationLevel::AllowInvalidValues);
137-
if ($fileLocation !== null) {
138-
$securityTxt->setFileLocation($fileLocation);
139-
}
140158
for ($lineNumber = 1; $lineNumber <= count($lines); $lineNumber++) {
141159
$line = trim($lines[$lineNumber - 1]);
142160
if (!str_ends_with($lines[$lineNumber - 1], "\n")) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
declare(strict_types = 1);
3+
4+
namespace Spaze\SecurityTxt\Violations;
5+
6+
use Spaze\SecurityTxt\SecurityTxtContentType;
7+
8+
final class SecurityTxtContentNotUtf8 extends SecurityTxtSpecViolation
9+
{
10+
11+
public function __construct()
12+
{
13+
$utf8 = strtoupper(SecurityTxtContentType::CHARSET);
14+
parent::__construct(
15+
func_get_args(),
16+
'The file content is not encoded in %s',
17+
[$utf8],
18+
'draft-foudil-securitytxt-00',
19+
null,
20+
'Re-encode the file in %s',
21+
[$utf8],
22+
'4',
23+
);
24+
}
25+
26+
}

tests/Parser/SecurityTxtParserTest.phpt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use Spaze\SecurityTxt\Validator\SecurityTxtValidator;
2424
use Spaze\SecurityTxt\Violations\SecurityTxtBugBountyWrongCase;
2525
use Spaze\SecurityTxt\Violations\SecurityTxtBugBountyWrongValue;
2626
use Spaze\SecurityTxt\Violations\SecurityTxtContactNotHttps;
27+
use Spaze\SecurityTxt\Violations\SecurityTxtContentNotUtf8;
2728
use Spaze\SecurityTxt\Violations\SecurityTxtContentTypeInvalid;
2829
use Spaze\SecurityTxt\Violations\SecurityTxtContentTypeWrongCharset;
2930
use Spaze\SecurityTxt\Violations\SecurityTxtCsafNotHttps;
@@ -279,6 +280,21 @@ final class SecurityTxtParserTest extends TestCase
279280
}
280281

281282

283+
public function testParseStringContentNotUtf8(): void
284+
{
285+
$parseResult = $this->securityTxtParser->parseString("\xFF\xFE");
286+
$errors = $parseResult->getFileErrors();
287+
Assert::count(1, $errors);
288+
Assert::type(SecurityTxtContentNotUtf8::class, $errors[0]);
289+
Assert::same([], $parseResult->getFileWarnings());
290+
Assert::same([], $parseResult->getLineErrors());
291+
Assert::same([], $parseResult->getLineWarnings());
292+
Assert::true($parseResult->hasErrors());
293+
Assert::false($parseResult->hasWarnings());
294+
Assert::false($parseResult->isValid());
295+
}
296+
297+
282298
public function testParseStringNoEol(): void
283299
{
284300
$contents = 'Expires: ' . (new DateTime('+3 months'))->format(SecurityTxtExpires::FORMAT) . "\nContact: https://foo.example/";

0 commit comments

Comments
 (0)