我在项目中的 Symfony 表单中发现了一个安全问题。该项目包括一个用于删除特定票证的表单。但是,我注意到可以使用浏览器开发人员工具修改表单以提交删除与预期不同的项目的请求。
## 展示漏洞:
1.
使用 Chrome 开发者工具检查 delete_ticket 表单的 HTML 代码,并编辑表单操作 URL 以指向不同的工单,例如“/project/4/ticket/3/delete”,然后更改隐藏输入的值表示票证 ID 值的字段为 3。
2.
点击“删除工单”按钮提交表格并确认删除。
3.
观察系统删除了 id 为 3 的工单,该工单与原本 id 为 6 的工单不同。
## 预期行为:
表单应该只能删除原始id(本例中id=6)的工单,任何修改表单数据的尝试都应该被拒绝,以维护系统安全。
## 实际行为:
该表单允许修改操作 URL 和 id 的隐藏输入字段,使攻击者能够删除系统中的任何项目,而不仅仅是他们最初尝试删除的特定项目。
任何人都可以建议修复此安全漏洞并确保表单只能删除预期项目而不允许攻击者操纵操作 URL 的最佳方法吗?此外,可以采取哪些措施来增强整体表单安全并防止 Symfony 项目中的此类攻击?
## 代码:
/** _list_projects.html.twig: */
{% if is_granted('DELETE_TICKET', project) %}
{{ form(ticket.delete_form) }}
{% endif %}
/** src/Controller/ProjectController.php: */
#[Route('/project/view/{id}', requirements: ['id' => '\d+'], name: 'view_project')]
public function viewProject(Request $request, Project $project = null, TicketRepository $ticketRepository): Response
{
if (!$project || $project->getTeam() !== $user->getTeam() || !$this->isGranted('VIEW_PROJECT', $project)) {
// Redirect/render access denied page
}
foreach ($ticketPaginator->getIterator() as $ticket) {
$tickets[] = [
'ticket' => $ticket,
'delete_form' => $this->createForm(DeleteTicketType::class, $ticket, [
'action' => $this->generateUrl('delete_ticket', [
'project' => $project->getId(),
'ticket' => $ticket->getId(),
]),
])->createView(),
];
}
return $this->render('project/view_project.html.twig', [
'project' => $project,
'tickets' => $tickets,
]);
}
/** src/Controller/TicketController.php: */
#[Route('/project/{project}/ticket/{ticket}/delete', requirements: ['project' => '\d+', 'ticket' => '\d+'], methods: ['POST'], name: 'delete_ticket')]
public function deleteTicket(Request $request, Project $project, Ticket $ticket = null)
{
// ...
if (!$project || !$ticket || !$this->isGranted('DELETE_TICKET', $project)) {
// Redirect/render access denied page
}
$form = $this->createForm(DeleteTicketType::class, $ticket);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// Get the ticket ID from the submitted form data.
$ticketId = $form->get('id')->getData();
// Verify if the provided ticket ID is numeric and matches the currently loaded ticket.
if (is_numeric($ticketId) && $ticketId == $ticket->getId()) {
// The ticket ID is valid, remove the ticket from the database
$this->entityManager->remove($ticket);
$this->entityManager->flush();
return $this->redirectToRoute('view_project', ['id' => $project->getId()]);
} else {
// Redirect/render something went rong template
}
}
// The form is not valid
// Display an access denied error
return $this->render('ticket/delete_ticket.html.twig');
}
/** src/Form/DeleteTicketType.php: */
class DeleteTicketType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('id', HiddenType::class, [
'mapped' => 'false',
'disabled' => true,
])
->add('deleteTicket', SubmitType::class, [
'attr' => [
'class' => 'btn-danger',
'onclick' => 'return confirm("Are you sure you want to delete the ticket ' . $options['data']->getName() . '?")',
],
]);
}
// ...
}
/** Original form code to delete a ticket in /project/view/4 page: */
<form name="delete_ticket" method="post" action="/project/4/ticket/6/delete">
<input type="hidden" id="delete_ticket_id" name="delete_ticket[id]" disabled="disabled" value="6">
<button type="submit" id="delete_ticket_deleteTicket" name="delete_ticket[deleteTicket]" class="btn-danger btn" onclick="return confirm("Are you sure you want to delete the ticket Ticket 6?")">
Delete ticket
</button>
<input type="hidden" id="delete_ticket__token" name="delete_ticket[_token]" value="880.gn90zml7-gqeAOZqJVR4GPfB3fBT7OQjlkAU7lqSsRI.zRsFtDYeu038aZM-R2ZVII-U7sNkoIlV9Hl-qRzl0FzyODaLUCm_OtBUkQ">
</form>
/** Modified form code to delete a ticket in /project/view/4 page: */
<form name="delete_ticket" method="post" action="/project/4/ticket/3/delete">
<input type="hidden" id="delete_ticket_id" name="delete_ticket[id]" disabled="disabled" value="3">
<!-- // ... -->
</form>
编辑2:添加ProjectVoter.php
class ProjectVoter extends Voter
{
public const DELETE_TICKET = 'DELETE_TICKET';
protected function supports(string $attribute, mixed $subject): bool
{
return in_array($attribute, [
self::DELETE_TICKET,
])
&& $subject instanceof \App\Entity\Project;
}
protected function voteOnAttribute(string $attribute, mixed $project, TokenInterface $token): bool
{
/**
* @var \App\Entity\User $user
*/
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
// ...
case self::DELETE_TICKET:
return $user->hasPermission($user::PERMISSIONS['TEAM_LEAD']) || $project->isManagedBy($user);
break;
}
return false;
}
}
首先,您已经检查了用户是否有权删除项目。您的选民保护您的票证不被匿名用户删除(例如)。如果有些票可以删除而另一些则不能,那么您可以提高投票者的水平。
那么,这不是 Symfony 安全问题,而是您的代码导致的问题。如果 id 是敏感数据,请勿转发。您可以通过对称算法来保护 id 参数。简而言之:加密它。
在您的表单中,您在验证过程中添加了一个步骤。您使用对称算法“恢复”id 并检查该值是否为整数。如果没有,你就会抛出一个错误。 这是关于对称算法的SO讨论。这篇文章向您展示了“不安全的加密货币”(没有身份验证)和“安全的加密货币”(有身份验证)。