fix(back): 拒绝 nested back 参数避免回退 URL 膨胀(plans/platform-orders)

This commit is contained in:
萝卜
2026-03-14 02:07:04 +00:00
parent 6c8d78d981
commit 56bf040252
4 changed files with 61 additions and 6 deletions

View File

@@ -149,7 +149,12 @@ class PlanController extends Controller
$back = (string) $request->query('back', ''); $back = (string) $request->query('back', '');
// back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。 // back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。
$safeBack = (str_starts_with($back, '/') && !preg_match('/["\'<>]/', $back)) ? $back : ''; $safeBack = (str_starts_with($back, '/')
&& !preg_match('/["\'<>]/', $back)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $back))
? $back
: '';
return view('admin.plans.form', [ return view('admin.plans.form', [
'plan' => new Plan(), 'plan' => new Plan(),
@@ -169,7 +174,12 @@ class PlanController extends Controller
$back = (string) $request->input('back', ''); $back = (string) $request->input('back', '');
// back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。 // back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。
$safeBack = (str_starts_with($back, '/') && !preg_match('/["\'<>]/', $back)) ? $back : ''; $safeBack = (str_starts_with($back, '/')
&& !preg_match('/["\'<>]/', $back)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $back))
? $back
: '';
$plan = Plan::query()->create($data); $plan = Plan::query()->create($data);
@@ -186,7 +196,12 @@ class PlanController extends Controller
$back = (string) $request->query('back', ''); $back = (string) $request->query('back', '');
// back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。 // back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。
$safeBack = (str_starts_with($back, '/') && !preg_match('/["\'<>]/', $back)) ? $back : ''; $safeBack = (str_starts_with($back, '/')
&& !preg_match('/["\'<>]/', $back)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $back))
? $back
: '';
return view('admin.plans.form', [ return view('admin.plans.form', [
'plan' => $plan, 'plan' => $plan,
@@ -226,7 +241,12 @@ class PlanController extends Controller
$back = (string) $request->input('back', ''); $back = (string) $request->input('back', '');
// back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。 // back 需为站内相对路径,并拒绝引号/尖括号,避免后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。
$safeBack = (str_starts_with($back, '/') && !preg_match('/["\'<>]/', $back)) ? $back : ''; $safeBack = (str_starts_with($back, '/')
&& !preg_match('/["\'<>]/', $back)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $back))
? $back
: '';
$plan->update($data); $plan->update($data);

View File

@@ -45,7 +45,10 @@ class PlatformOrderController extends Controller
// back 安全阀:必须为站内相对路径,并拒绝引号/尖括号。 // back 安全阀:必须为站内相对路径,并拒绝引号/尖括号。
// 说明form 页会把 defaults.back 透传到 hidden input 与返回按钮;因此这里提前清洗,避免 unsafe back 在页面中出现。 // 说明form 页会把 defaults.back 透传到 hidden input 与返回按钮;因此这里提前清洗,避免 unsafe back 在页面中出现。
$incomingBack = (string) ($defaults['back'] ?? ''); $incomingBack = (string) ($defaults['back'] ?? '');
$defaults['back'] = (str_starts_with($incomingBack, '/') && !preg_match('/["\'<>]/', $incomingBack)) $defaults['back'] = (str_starts_with($incomingBack, '/')
&& !preg_match('/["\'<>]/', $incomingBack)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $incomingBack))
? $incomingBack ? $incomingBack
: ''; : '';
@@ -134,7 +137,12 @@ class PlatformOrderController extends Controller
$back = (string) ($data['back'] ?? ''); $back = (string) ($data['back'] ?? '');
// back 需为站内相对路径,并拒绝引号/尖括号,避免在后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。 // back 需为站内相对路径,并拒绝引号/尖括号,避免在后续页面以 `{!! !!}` 原样输出时引入 XSS 风险。
$safeBack = (str_starts_with($back, '/') && !preg_match('/["\'<>]/', $back)) ? $back : ''; $safeBack = (str_starts_with($back, '/')
&& !preg_match('/["\'<>]/', $back)
// back 本身不应再包含 back避免无限嵌套导致 URL 膨胀)
&& !preg_match('/(?:^|[?&])back=/', $back))
? $back
: '';
$redirectUrl = '/admin/platform-orders/' . $order->id; $redirectUrl = '/admin/platform-orders/' . $order->id;
if ($safeBack !== '') { if ($safeBack !== '') {

View File

@@ -33,6 +33,19 @@ class AdminPlanControllerBackValidationTest extends TestCase
$res->assertDontSee($unsafeBack, false); $res->assertDontSee($unsafeBack, false);
} }
public function test_create_should_not_echo_back_when_back_contains_nested_back_param(): void
{
$this->loginAsPlatformAdmin();
$nestedBack = '/admin/platform-orders?status=pending&back=/admin/plans';
$res = $this->get('/admin/plans/create?back=' . urlencode($nestedBack));
$res->assertOk();
$res->assertDontSee('name="back"', false);
$res->assertDontSee($nestedBack, false);
}
public function test_store_should_ignore_unsafe_back_and_redirect_to_index(): void public function test_store_should_ignore_unsafe_back_and_redirect_to_index(): void
{ {
$this->loginAsPlatformAdmin(); $this->loginAsPlatformAdmin();

View File

@@ -35,4 +35,18 @@ class AdminPlatformOrderCreateBackValidationTest extends TestCase
// 返回按钮应回退到默认列表 // 返回按钮应回退到默认列表
$res->assertSee('href="/admin/platform-orders"', false); $res->assertSee('href="/admin/platform-orders"', false);
} }
public function test_create_should_not_echo_back_when_back_contains_nested_back_param(): void
{
$this->loginAsPlatformAdmin();
$nestedBack = '/admin/site-subscriptions?status=activated&back=/admin/platform-orders';
$res = $this->get('/admin/platform-orders/create?back=' . urlencode($nestedBack));
$res->assertOk();
$res->assertDontSee('name="back"', false);
$res->assertDontSee($nestedBack, false);
$res->assertSee('href="/admin/platform-orders"', false);
}
} }