Skip to content

Commit 62c3a2a

Browse files
committed
fix(auth): harden reset password page policy serialization with Js::from
Wrap shape_pattern and allowed_special_characters in Js::from() in reset.blade.php. Without it, {{ }} encodes & as & inside the script block, corrupting the regex character class and causing the client to accept passwords the server rejects. Add PasswordPolicyRenderingTest to guard against this class of rendering bug.
1 parent 93a0d4c commit 62c3a2a

2 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎resources/views/auth/passwords/reset.blade.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
const passwordPolicy = {
2323
min_length: {{ Config::get("auth.password_min_length") }},
2424
max_length: {{ Config::get("auth.password_max_length") }},
25-
shape_pattern: '{{ Config::get("auth.password_shape_pattern") }}',
26-
allowed_special_characters: '{{ Config::get("auth.password_allowed_special_characters") }}',
25+
shape_pattern: {{ Illuminate\Support\Js::from(Config::get("auth.password_shape_pattern")) }},
26+
allowed_special_characters: {{ Illuminate\Support\Js::from(Config::get("auth.password_allowed_special_characters")) }},
2727
allowed_special_characters_text: {{ Illuminate\Support\Js::from(Config::get("auth.password_allowed_special_characters_text")) }},
2828
shape_warning: '{{ Config::get("auth.password_shape_warning") }}',
2929
shape_list: {{ Illuminate\Support\Js::from(Config::get("auth.password_shape_list")) }}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php namespace Tests;
2+
/**
3+
* Copyright 2026 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
15+
use Illuminate\Support\Facades\Config;
16+
use Illuminate\Support\ViewErrorBag;
17+
18+
class PasswordPolicyRenderingTest extends TestCase
19+
{
20+
/**
21+
* The regex the browser receives must classify passwords exactly as the
22+
* server-side one does. Probes are all 10-30 chars so length is never the
23+
* discriminator — only the special-character class is under test.
24+
*/
25+
public function test_rendered_reset_pattern_agrees_with_server_pattern(): void
26+
{
27+
$html = view('auth.passwords.reset', [
28+
'token' => 'irrelevant-for-rendering',
29+
'email' => 'probe@example.com',
30+
'errors' => new ViewErrorBag(),
31+
])->render();
32+
33+
$this->assertSame(1, preg_match('/shape_pattern:\s*(.+?),\R/', $html, $m));
34+
35+
// Resolve the \uXXXX escapes a JS parser would resolve in the literal.
36+
// Trim both single quotes ({{ }} format) and double quotes (Js::from format).
37+
$client = preg_replace_callback(
38+
'/\\\\u([0-9a-fA-F]{4})/',
39+
fn ($u) => mb_chr(hexdec($u[1])),
40+
trim($m[1], "\"'")
41+
);
42+
$server = Config::get('auth.password_shape_pattern');
43+
44+
foreach (['Passwordma1', 'Passw0rdabc', ';Passw0rdaa', 'Valid1Pass!'] as $probe) {
45+
$this->assertSame(
46+
(bool) preg_match("/{$server}/", $probe),
47+
(bool) preg_match("/{$client}/", $probe),
48+
"rendered and server patterns disagree on '{$probe}'"
49+
);
50+
}
51+
}
52+
}

0 commit comments

Comments
 (0)