Skip to content

Commit a00ebf3

Browse files
committed
better cookie value validation
1 parent 13ed55e commit a00ebf3

File tree

3 files changed

+93
-5
lines changed

3 files changed

+93
-5
lines changed

phpstan-baseline.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Dead catch \- JsonException is never thrown in the try block\.$#'
5+
identifier: catch.neverThrown
6+
count: 1
7+
path: src/CookieSolution.php
8+
9+
-
10+
message: '#^Parameter \#1 \$view of function view expects view\-string\|null, string given\.$#'
11+
identifier: argument.type
12+
count: 1
13+
path: src/CookieSolution.php

src/CookieSolution.php

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
namespace Keepsuit\CookieSolution;
44

55
use Carbon\Carbon;
6+
use DateTimeInterface;
67
use Illuminate\Support\Arr;
78
use Illuminate\Support\Collection;
89
use Illuminate\Support\Facades\File;
10+
use Illuminate\Support\Facades\Validator;
911
use Illuminate\Support\HtmlString;
1012
use Illuminate\Support\Str;
1113
use Illuminate\Support\Stringable;
14+
use Illuminate\Validation\ValidationException;
1215

1316
class CookieSolution
1417
{
@@ -41,22 +44,44 @@ public function status(): CookieSolutionStatus
4144

4245
$json = \Illuminate\Support\Facades\Cookie::get(config('cookie-solution.cookie_name'));
4346

44-
if ($json === null) {
47+
if (! is_string($json)) {
4548
return $this->status = CookieSolutionStatus::default();
4649
}
4750

4851
try {
4952
$value = json_decode($json, true, JSON_THROW_ON_ERROR);
53+
} catch (\JsonException) {
54+
return $this->status = CookieSolutionStatus::default();
55+
}
5056

51-
if ($this->configDigest() !== ($value['digest'] ?? null)) {
57+
try {
58+
$validator = Validator::make($value, [
59+
'digest' => ['required', 'string'],
60+
'timestamp' => ['required', sprintf('date_format:%s', DateTimeInterface::ATOM)],
61+
'purposes' => ['required', 'array'],
62+
'purposes.*' => ['required', 'boolean'],
63+
]);
64+
65+
/**
66+
* @var array{
67+
* digest: string,
68+
* timestamp: string,
69+
* purposes: array<string, bool>
70+
* } $validated
71+
*/
72+
$validated = $validator->validated();
73+
74+
if ($this->configDigest() !== $validated['digest']) {
5275
return $this->status = CookieSolutionStatus::default();
5376
}
5477

5578
return $this->status = new CookieSolutionStatus(
56-
timestamp: Carbon::parse($value['timestamp']),
57-
purposes: $value['purposes'],
79+
timestamp: Carbon::createFromFormat(DateTimeInterface::ATOM, $validated['timestamp']),
80+
purposes: collect($validated['purposes'])
81+
->filter(fn (bool $active, string $key) => CookiePurpose::tryFrom($key) !== null)
82+
->all(),
5883
);
59-
} catch (\JsonException $e) {
84+
} catch (ValidationException) {
6085
return $this->status = CookieSolutionStatus::default();
6186
}
6287
}

tests/CookieSolutionStatus.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,53 @@
8383
->purposeStatus(CookiePurpose::NECESSARY)->toBeNull()
8484
->purposeStatus(CookiePurpose::PREFERENCES)->toBeNull();
8585
});
86+
87+
it('load default status when config date is malformed', function () {
88+
\Spatie\TestTime\TestTime::freeze();
89+
90+
$request = app('request');
91+
assert($request instanceof \Illuminate\Http\Request);
92+
$request->cookies->set('laravel_cookie_solution', json_encode([
93+
'timestamp' => sprintf('%s<script>alert(1)</script>', now()->subHour()->toIso8601String()),
94+
'digest' => CookieSolution::getConfig()['digest'],
95+
'purposes' => [
96+
'statistics' => true,
97+
'marketing' => false,
98+
],
99+
]));
100+
101+
$status = CookieSolution::status();
102+
103+
expect($status)
104+
->toBeInstanceOf(CookieSolutionStatus::class)
105+
->timestamp->toIso8601String()->toBe(now()->toIso8601String())
106+
->purposeStatus(CookiePurpose::STATISTICS)->toBeNull()
107+
->purposeStatus(CookiePurpose::MARKETING)->toBeNull()
108+
->purposeStatus(CookiePurpose::NECESSARY)->toBeNull()
109+
->purposeStatus(CookiePurpose::PREFERENCES)->toBeNull();
110+
});
111+
112+
it('load default status when config purposes are malformed', function () {
113+
\Spatie\TestTime\TestTime::freeze();
114+
115+
$request = app('request');
116+
assert($request instanceof \Illuminate\Http\Request);
117+
$request->cookies->set('laravel_cookie_solution', json_encode([
118+
'timestamp' => now()->subHour()->toIso8601String(),
119+
'digest' => CookieSolution::getConfig()['digest'],
120+
'purposes' => [
121+
'statistics' => true,
122+
'marketing' => 'invalid',
123+
],
124+
]));
125+
126+
$status = CookieSolution::status();
127+
128+
expect($status)
129+
->toBeInstanceOf(CookieSolutionStatus::class)
130+
->timestamp->toIso8601String()->toBe(now()->toIso8601String())
131+
->purposeStatus(CookiePurpose::STATISTICS)->toBeNull()
132+
->purposeStatus(CookiePurpose::MARKETING)->toBeNull()
133+
->purposeStatus(CookiePurpose::NECESSARY)->toBeNull()
134+
->purposeStatus(CookiePurpose::PREFERENCES)->toBeNull();
135+
});

0 commit comments

Comments
 (0)