Skip to content

Commit a2b0a0f

Browse files
committed
Merge branch '5.x' into 6.x
2 parents 0e9f860 + eecc9ed commit a2b0a0f

4 files changed

Lines changed: 114 additions & 10 deletions

File tree

src/Data/DataCollection.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
use Illuminate\Support\Collection as IlluminateCollection;
88
use Statamic\Exceptions\MethodNotFoundException;
99
use Statamic\Facades\Compare;
10+
use Statamic\Query\ResolveValue;
1011
use Statamic\Search\PlainResult;
1112
use Statamic\Search\Result;
12-
use Statamic\Support\Str;
1313

1414
/**
1515
* An abstract collection of data types.
@@ -100,15 +100,7 @@ protected function getSortableValue($sort, $item)
100100
$item = $item->getSearchable() ?? $item;
101101
}
102102

103-
if (method_exists($item, $method = Str::camel($sort))) {
104-
return $this->normalizeSortableValue(call_user_func([$item, $method]));
105-
}
106-
107-
if (method_exists($item, 'value')) {
108-
return $this->normalizeSortableValue($item->value($sort));
109-
}
110-
111-
return $this->normalizeSortableValue($item->get($sort));
103+
return $this->normalizeSortableValue((new ResolveValue)($item, $sort));
112104
}
113105

114106
/**

src/Tokens/FileTokenRepository.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,19 @@ class FileTokenRepository extends TokenRepository
1111
{
1212
public function make(?string $token, string $handler, array $data = []): TokenContract
1313
{
14+
if ($token && ! $this->isValidTokenName($token)) {
15+
throw new \InvalidArgumentException("Invalid token name [{$token}].");
16+
}
17+
1418
return app()->makeWith(TokenContract::class, compact('token', 'handler', 'data'));
1519
}
1620

1721
public function find(string $token): ?TokenContract
1822
{
23+
if (! $this->isValidTokenName($token)) {
24+
return null;
25+
}
26+
1927
$path = storage_path('statamic/tokens/'.$token.'.yaml');
2028

2129
if (! File::exists($path)) {
@@ -55,6 +63,11 @@ public function collectGarbage(): void
5563
->each->delete();
5664
}
5765

66+
private function isValidTokenName(string $token): bool
67+
{
68+
return (bool) preg_match('/^[A-Za-z0-9_-]+\z/', $token);
69+
}
70+
5871
private function makeFromPath(string $path): FileToken
5972
{
6073
$yaml = YAML::file($path)->parse();

tests/Data/DataCollectionTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Tests\Data;
44

55
use PHPUnit\Framework\Attributes\Test;
6+
use Statamic\Contracts\Search\Searchable;
67
use Statamic\Data\DataCollection;
8+
use Statamic\Search\Result;
79
use Tests\TestCase;
810

911
class DataCollectionTest extends TestCase
@@ -31,4 +33,69 @@ public function it_sorts_by_first_item_in_arrays()
3133

3234
$this->assertEquals([1, 3, 2], $collection->multisort('foos')->pluck('id')->all());
3335
}
36+
37+
#[Test]
38+
public function sorting_by_unsafe_method_does_not_invoke_it()
39+
{
40+
$a = new DataCollectionTestObject('alfa');
41+
$b = new DataCollectionTestObject('bravo');
42+
43+
$collection = new DataCollection([$a, $b]);
44+
45+
$collection->multisort('delete');
46+
47+
$this->assertFalse($a->deleted);
48+
$this->assertFalse($b->deleted);
49+
}
50+
51+
#[Test]
52+
public function sorting_search_results_by_unsafe_method_does_not_invoke_it()
53+
{
54+
$a = new DataCollectionTestObject('alfa');
55+
$b = new DataCollectionTestObject('bravo');
56+
57+
$collection = new DataCollection([
58+
new Result($a, 'test'),
59+
new Result($b, 'test'),
60+
]);
61+
62+
$collection->multisort('delete');
63+
64+
$this->assertFalse($a->deleted);
65+
$this->assertFalse($b->deleted);
66+
}
67+
}
68+
69+
class DataCollectionTestObject implements Searchable
70+
{
71+
public bool $deleted = false;
72+
73+
public function __construct(public string $title)
74+
{
75+
}
76+
77+
public function delete()
78+
{
79+
$this->deleted = true;
80+
}
81+
82+
public function get($key)
83+
{
84+
return $this->{$key} ?? null;
85+
}
86+
87+
public function getSearchValue(string $field)
88+
{
89+
return $this->{$field} ?? null;
90+
}
91+
92+
public function getSearchReference(): string
93+
{
94+
return 'test::'.$this->title;
95+
}
96+
97+
public function toSearchResult(): Result
98+
{
99+
return new Result($this, 'test');
100+
}
34101
}

tests/Tokens/TokenRepositoryTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Facades\Statamic\Tokens\Generator;
66
use Illuminate\Support\Carbon;
77
use Illuminate\Support\Collection;
8+
use PHPUnit\Framework\Attributes\DataProvider;
89
use PHPUnit\Framework\Attributes\Test;
910
use Statamic\Contracts\Tokens\Token;
1011
use Statamic\Facades\File;
@@ -129,6 +130,37 @@ public function attempting_to_find_a_non_existent_token_returns_null()
129130
$this->assertNull($this->tokens->find('missing-token'));
130131
}
131132

133+
#[Test]
134+
public function it_prevents_path_traversal_in_find()
135+
{
136+
File::put(storage_path('statamic/evil.yaml'), "handler: 'Handler'\nexpires_at: 9999999999\ndata: []");
137+
138+
$this->assertNull($this->tokens->find('../evil'));
139+
}
140+
141+
#[Test]
142+
#[DataProvider('invalidTokenNameProvider')]
143+
public function it_throws_when_making_a_token_with_an_invalid_name($token)
144+
{
145+
$this->expectException(\InvalidArgumentException::class);
146+
147+
$this->tokens->make($token, 'Handler');
148+
}
149+
150+
public static function invalidTokenNameProvider()
151+
{
152+
return [
153+
'parent traversal' => ['../evil'],
154+
'backslash traversal' => ['..\\evil'],
155+
'nested traversal' => ['foo/../../evil'],
156+
'forward slash' => ['foo/evil'],
157+
'dots only' => ['..'],
158+
'absolute path' => ['/etc/passwd'],
159+
'windows drive' => ['C:\\evil'],
160+
'trailing newline' => ["evil\n"],
161+
];
162+
}
163+
132164
#[Test]
133165
public function it_deletes_expired_tokens()
134166
{

0 commit comments

Comments
 (0)