diff --git a/app/Http/Controllers/Admin/PlanController.php b/app/Http/Controllers/Admin/PlanController.php index 4915ee0..232591a 100644 --- a/app/Http/Controllers/Admin/PlanController.php +++ b/app/Http/Controllers/Admin/PlanController.php @@ -149,7 +149,12 @@ class PlanController extends Controller $back = (string) $request->query('back', ''); // 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', [ 'plan' => new Plan(), @@ -169,7 +174,12 @@ class PlanController extends Controller $back = (string) $request->input('back', ''); // 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); @@ -186,7 +196,12 @@ class PlanController extends Controller $back = (string) $request->query('back', ''); // 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', [ 'plan' => $plan, @@ -226,7 +241,12 @@ class PlanController extends Controller $back = (string) $request->input('back', ''); // 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); diff --git a/app/Http/Controllers/Admin/PlatformOrderController.php b/app/Http/Controllers/Admin/PlatformOrderController.php index 4e11942..f75ab94 100644 --- a/app/Http/Controllers/Admin/PlatformOrderController.php +++ b/app/Http/Controllers/Admin/PlatformOrderController.php @@ -45,7 +45,10 @@ class PlatformOrderController extends Controller // back 安全阀:必须为站内相对路径,并拒绝引号/尖括号。 // 说明:form 页会把 defaults.back 透传到 hidden input 与返回按钮;因此这里提前清洗,避免 unsafe 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 : ''; @@ -134,7 +137,12 @@ class PlatformOrderController extends Controller $back = (string) ($data['back'] ?? ''); // 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; if ($safeBack !== '') { diff --git a/tests/Feature/AdminPlanControllerBackValidationTest.php b/tests/Feature/AdminPlanControllerBackValidationTest.php index c5d68ca..819effd 100644 --- a/tests/Feature/AdminPlanControllerBackValidationTest.php +++ b/tests/Feature/AdminPlanControllerBackValidationTest.php @@ -33,6 +33,19 @@ class AdminPlanControllerBackValidationTest extends TestCase $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 { $this->loginAsPlatformAdmin(); diff --git a/tests/Feature/AdminPlatformOrderCreateBackValidationTest.php b/tests/Feature/AdminPlatformOrderCreateBackValidationTest.php index c331288..ba4cc98 100644 --- a/tests/Feature/AdminPlatformOrderCreateBackValidationTest.php +++ b/tests/Feature/AdminPlatformOrderCreateBackValidationTest.php @@ -35,4 +35,18 @@ class AdminPlatformOrderCreateBackValidationTest extends TestCase // 返回按钮应回退到默认列表 $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); + } }