Skip to content

Commit b31a99c

Browse files
Merge pull request #13 from macropay-solutions/retroactive-fix-CVE-2025-27515
retroactive-fix-CVE-2025-27515
2 parents b0ccffd + 5c268cc commit b31a99c

File tree

2 files changed

+254
-0
lines changed

2 files changed

+254
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Register
9191

9292
\MacropaySolutions\LaravelCrudWizard\Providers\CrudProvider
9393

94+
\MacropaySolutions\LaravelCrudWizard\Providers\ValidationServiceProvider instead of Illuminate\Validation\ValidationServiceProvider::class if you are using **illuminate/validation < 11.44.1 to solve CVE-2025-27515 security issue**
95+
9496
\MacropaySolutions\LaravelCrudWizard\Http\Middleware\UnescapedJsonMiddleware::class in lumen or laravel.
9597

9698
Create a constant in your code
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
<?php
2+
3+
namespace MacropaySolutions\LaravelCrudWizard\Providers;
4+
5+
use Composer\InstalledVersions;
6+
use Illuminate\Contracts\Translation\Translator;
7+
use Illuminate\Contracts\Validation\DataAwareRule;
8+
use Illuminate\Contracts\Validation\ValidatorAwareRule;
9+
use Illuminate\Support\Collection;
10+
use Illuminate\Support\Str;
11+
use Illuminate\Validation\Factory;
12+
use Illuminate\Validation\Rules\Password;
13+
use Illuminate\Validation\ValidationServiceProvider as BaseValidationServiceProvider;
14+
use Illuminate\Validation\Validator;
15+
use MacropaySolutions\LaravelCrudWizard\Helpers\GeneralHelper;
16+
17+
/**
18+
* Solves moderate security issue CVE-2025-27515 retrospectively
19+
* When using wildcard validation to validate a given file or image field array (files.*),
20+
* a user-crafted malicious request could potentially bypass the validation rules.
21+
* based on https://github.com/laravel/framework/pull/54845/files
22+
*/
23+
class ValidationServiceProvider extends BaseValidationServiceProvider
24+
{
25+
/**
26+
* @inheritDoc
27+
*/
28+
protected function registerValidationFactory(): void
29+
{
30+
if (\version_compare(InstalledVersions::getVersion('illuminate/validation'), '11.44.1', '>=')) {
31+
parent::registerValidationFactory();
32+
33+
return;
34+
}
35+
36+
$this->app->singleton('validator', function ($app) {
37+
$validator = new Factory($app['translator'], $app);
38+
$validator->resolver(
39+
fn ($translator, $data, $rules, $messages, $customAttributes): Validator => new class (
40+
$translator,
41+
$data,
42+
$rules,
43+
$messages,
44+
$customAttributes
45+
) extends Validator {
46+
/**
47+
* The current random hash for the validator.
48+
*
49+
* @var string
50+
*/
51+
protected static string $placeholderHash;
52+
53+
public function __construct(
54+
Translator $translator,
55+
array $data,
56+
array $rules,
57+
array $messages = [],
58+
array $customAttributes = []
59+
) {
60+
if (! isset(static::$placeholderHash)) {
61+
static::$placeholderHash = Str::random();
62+
}
63+
64+
parent::__construct($translator, $data, $rules, $messages, $customAttributes);
65+
}
66+
67+
/**
68+
* @inheritDoc
69+
*/
70+
public function parseData(array $data): array
71+
{
72+
$newData = [];
73+
74+
foreach ($data as $key => $value) {
75+
if (\is_array($value)) {
76+
$value = $this->parseData($value);
77+
}
78+
79+
$key = \str_replace(
80+
['.', '*'],
81+
['__dot__' . static::$placeholderHash, '__asterisk__' . static::$placeholderHash],
82+
$key
83+
);
84+
85+
$newData[$key] = $value;
86+
}
87+
88+
return $newData;
89+
}
90+
91+
/**
92+
* @inheritDoc
93+
*/
94+
protected function replacePlaceholderInString(string $value): string
95+
{
96+
return \str_replace(
97+
['__dot__' . static::$placeholderHash, '__asterisk__' . static::$placeholderHash],
98+
['.', '*'],
99+
$value
100+
);
101+
}
102+
103+
/**
104+
* Replace each field parameter dot placeholder with dot.
105+
* from version 11
106+
*/
107+
protected function replaceDotPlaceholderInParameters(array $parameters): array
108+
{
109+
return \array_map(function ($field) {
110+
return \str_replace('__dot__' . static::$placeholderHash, '.', $field);
111+
}, $parameters);
112+
}
113+
114+
/**
115+
* @inheritDoc
116+
*/
117+
protected function replaceDotInParameters(array $parameters): array
118+
{
119+
return \array_map(function ($field) {
120+
return \str_replace('\.', '__dot__' . static::$placeholderHash, $field);
121+
}, $parameters);
122+
}
123+
124+
/**
125+
* @inheritDoc
126+
*/
127+
protected function validateUsingCustomRule($attribute, $value, $rule): void
128+
{
129+
$originalAttribute = $this->replacePlaceholderInString($attribute);
130+
131+
if (\version_compare(GeneralHelper::app()->version(), '9.0.0', '<')) {
132+
$attribute = $rule instanceof Password ? $attribute : $originalAttribute;
133+
134+
$value = is_array($value) ? $this->replacePlaceholders($value) : $value;
135+
136+
if ($rule instanceof ValidatorAwareRule) {
137+
if ($attribute !== $originalAttribute) {
138+
$this->addCustomAttributes([
139+
$attribute => $this->customAttributes[$originalAttribute] ?? $originalAttribute,
140+
]);
141+
}
142+
143+
$rule->setValidator($this);
144+
}
145+
146+
if ($rule instanceof DataAwareRule) {
147+
$rule->setData($this->data);
148+
}
149+
150+
if (!$rule->passes($attribute, $value)) {
151+
$this->failedRules[$originalAttribute][get_class($rule)] = [];
152+
153+
$messages = $rule->message();
154+
155+
$messages = $messages ? (array) $messages : [get_class($rule)];
156+
157+
foreach ($messages as $message) {
158+
$this->messages->add($originalAttribute, $this->makeReplacements(
159+
$message, $originalAttribute, get_class($rule), []
160+
));
161+
}
162+
}
163+
164+
return;
165+
}
166+
167+
$attribute = match (true) {
168+
\class_exists($class = '\Illuminate\Validation\Rules\Email') && \is_a($rule, $class) =>
169+
$attribute,
170+
\class_exists($class = '\Illuminate\Validation\Rules\File') && \is_a($rule, $class) =>
171+
$attribute,
172+
$rule instanceof Password => $attribute,
173+
default => $originalAttribute,
174+
};
175+
176+
$value = is_array($value) ? $this->replacePlaceholders($value) : $value;
177+
178+
if ($rule instanceof ValidatorAwareRule) {
179+
if ($attribute !== $originalAttribute) {
180+
$this->addCustomAttributes([
181+
$attribute => $this->customAttributes[$originalAttribute] ?? $originalAttribute,
182+
]);
183+
}
184+
185+
$rule->setValidator($this);
186+
}
187+
188+
if ($rule instanceof DataAwareRule) {
189+
$rule->setData($this->data);
190+
}
191+
192+
if (!$rule->passes($attribute, $value)) {
193+
$ruleClass = \class_exists(
194+
$class = '\Illuminate\Validation\InvokableValidationRule'
195+
) && \is_a($rule, $class) ? \get_class($rule->{'invokable'}()) : \get_class($rule);
196+
197+
$this->failedRules[$originalAttribute][$ruleClass] = [];
198+
199+
$messages = $this->getFromLocalArray($originalAttribute, $ruleClass) ?? $rule->message();
200+
201+
$messages = $messages ? (array) $messages : [$ruleClass];
202+
203+
foreach ($messages as $key => $message) {
204+
$key = \is_string($key) ? $key : $originalAttribute;
205+
206+
$this->messages->add(
207+
$key,
208+
$this->makeReplacements($message, $key, $ruleClass, [])
209+
);
210+
}
211+
}
212+
}
213+
214+
/**
215+
* Get the validation rules with key placeholders removed.
216+
* from version 9
217+
*/
218+
public function getRulesWithoutPlaceholders(): array
219+
{
220+
return (new Collection($this->rules))->mapWithKeys(fn (mixed $value, string $key): array => [
221+
\str_replace('__dot__' . static::$placeholderHash, '\\.', $key) => $value,
222+
])->all();
223+
}
224+
225+
/**
226+
* @inheritDoc
227+
*/
228+
public function setRules(array $rules): static
229+
{
230+
$rules = (new Collection($rules))->mapWithKeys(function (mixed $value, string $key): array {
231+
return [\str_replace('\.', '__dot__' . static::$placeholderHash, $key) => $value];
232+
})->toArray();
233+
$this->initialRules = $rules;
234+
$this->rules = [];
235+
$this->addRules($rules);
236+
237+
return $this;
238+
}
239+
}
240+
);
241+
242+
// The validation presence verifier is responsible for determining the existence of
243+
// values in a given data collection which is typically a relational database or
244+
// other persistent data stores. It is used to check for "uniqueness" as well.
245+
if (isset($app['db'], $app['validation.presence'])) {
246+
$validator->setPresenceVerifier($app['validation.presence']);
247+
}
248+
249+
return $validator;
250+
});
251+
}
252+
}

0 commit comments

Comments
 (0)