跳到内容

Symfony 备忘单

简介

本备忘单旨在为使用 Symfony 框架构建应用程序的开发人员提供安全提示。它涵盖了常见的漏洞和最佳实践,以确保您的 Symfony 应用程序安全。

虽然 Symfony 提供了内置的安全机制,但开发人员仍需了解潜在的漏洞和最佳实践,以确保他们构建的应用程序是安全的。本指南旨在涵盖常见的安全问题,强调理解 Symfony 安全特性及其有效利用的重要性。无论您是 Symfony 的新手,还是寻求加强安全实践的经验丰富的开发人员,本文档都将是宝贵的资源。遵循此处概述的指南,您可以增强 Symfony 应用程序的安全性,并为用户和数据创建更安全的数字环境。

主要部分

跨站脚本 (XSS)

跨站脚本 (XSS) 是一种攻击类型,恶意 JavaScript 代码被注入到显示的变量中。例如,如果变量名 `name` 的值为 <script>alert('hello')</script>,并且我们在 HTML 中像这样显示它:Hello {{name}},那么当 HTML 渲染时,注入的脚本将被执行。

Symfony 默认附带 Twig 模板,通过使用 输出转义 来自动保护应用程序免受 XSS 攻击,它通过将变量包装在 {{ }} 语句中来转换包含特殊字符的变量。

<p>Hello {{name}}</p>
{# if 'name' is '<script>alert('hello!')</script>', Twig will output this:
'<p>Hello &lt;script&gt;alert(&#39;hello!&#39;)&lt;/script&gt;</p>' #}

如果您正在渲染一个受信任且包含 HTML 内容的变量,您可以使用 Twig raw 过滤器 来禁用输出转义。

<p>{{ product.title|raw }}</p>
{# if 'product.title' is 'Lorem <strong>Ipsum</strong>', Twig will output
exactly that instead of 'Lorem &lt;strong&gt;Ipsum&lt;/strong&gt;' #}

查阅 Twig 输出转义文档 以深入了解如何禁用特定块或整个模板的输出转义。

有关非 Symfony 特定的 XSS 防御信息,您可以参考 跨站脚本防御备忘单

跨站请求伪造 (CSRF)

Symfony Form 组件会自动在表单中包含 CSRF 令牌,提供内置的 CSRF 攻击防护。Symfony 会自动验证这些令牌,无需手动干预即可保护您的应用程序。

默认情况下,CSRF 令牌作为一个名为 _token 的隐藏字段添加,但这可以根据具体表单通过其他设置进行自定义。

use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class PostForm extends AbstractType
{

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            // ... 
            'csrf_protection' => true,  // enable/disable csrf protection for this form
            'csrf_field_name' => '_csrf_token',
            'csrf_token_id'   => 'post_item', // change arbitrary string used to generate
        ]);
    }

}

如果您不使用 Symfony Forms,可以自行生成和验证 CSRF 令牌。为此,您需要安装 symfony/security-csrf 组件。

composer install symfony/security-csrf

config/packages/framework.yaml 文件中启用/禁用 CSRF 保护

framework:
    csrf_protection: ~

接下来,考虑当 csrf_token() Twig 函数生成 CSRF 令牌时,以下 HTML Twig 模板

<form action="{{ url('delete_post', { id: post.id }) }}" method="post">
    <input type="hidden" name="token" value="{{ csrf_token('delete-post') }}">
    <button type="submit">Delete post</button>
</form>

然后您可以在控制器中使用 isCsrfTokenValid() 函数获取 CSRF 令牌的值

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController
{

    #[Route('/posts/{id}', methods: ['DELETE'], name: 'delete_post')]
    public function delete(Post $post, Request $request): Response 
    { 
        $token = $request->request->get('token')
        if($this->isCsrfTokenValid($token)) {
            // ...
        }

        // ...
    }
}

您可以在 跨站请求伪造 (CSRF) 备忘单 中找到更多与 Symfony 无关的 CSRF 信息。

SQL 注入

SQL 注入是一种安全漏洞,当攻击者能够以某种方式操纵 SQL 查询,使其可以执行任意 SQL 代码时发生。这可能允许攻击者查看、修改或删除数据库中的数据,从而可能导致未经授权的访问或数据丢失。

Symfony,特别是在与 Doctrine ORM(对象关系映射)一起使用时,通过预处理语句参数提供 SQL 注入防护。得益于此,误写未受保护的查询变得更加困难,但仍有可能。以下示例显示了 不安全的 DQL 用法

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ExampleController extends AbstractController {

    public function getPost(Request $request, EntityManagerInterface $em): Response
    {
        $id = $request->query->get('id');

        $dql = "SELECT p FROM App\Entity\Post p WHERE p.id = " . $id . ";";
        $query = $em->createQuery($dql);
        $post = $query->getSingleResult();

        // ...
    }
}

以下示例显示了提供 SQL 注入防护的 正确方法

  • 使用实体仓库内置方法
$id = $request->query->get('id');
$post = $em->getRepository(Post::class)->findOneBy(['id' => $id]);
  • 使用 Doctrine DQL 语言
$query = $em->createQuery("SELECT p FROM App\Entity\Post p WHERE p.id = :id");
$query->setParameter('id', $id);
$post = $query->getSingleResult();
  • 使用 DBAL 查询构建器
$qb = $em->createQueryBuilder();
$post = $qb->select('p')
            ->from('posts','p')
            ->where('id = :id')
            ->setParameter('id', $id)
            ->getQuery()
            ->getSingleResult();

有关 Doctrine 的更多信息,您可以参考 他们的文档。您还可以参考 SQL 注入防护备忘单 获取更多与 Symfony 或 Doctrine 无关的信息。

命令注入

命令注入发生在恶意代码被注入到应用程序系统并执行时。更多信息请参考 命令注入防御备忘单

考虑以下示例,其中使用 exec() 函数删除文件而未对输入进行任何转义

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Annotation\Route;

#[AsController]
class ExampleController 
{

    #[Route('/remove_file', methods: ['POST'])]
    public function removeFile(Request $request): Response
    {
        $filename =  $request->request->get('filename');
        exec(sprintf('rm %s', $filename));

        // ...
    }
}

在上述代码中,没有对用户输入进行验证。想象一下,如果用户提供像 test.txt && rm -rf . 这样的恶意值会发生什么。为了降低此风险,建议使用像 unlink() 这样的原生 PHP 函数,或 Symfony Filesystem 组件的 remove() 方法,而不是 exec()

对于与您情况相关的特定 PHP 文件系统函数,您可以参考 PHP 文档Symfony 文件系统组件文档

开放重定向

开放重定向是一种安全缺陷,当 Web 应用程序将用户重定向到未经验证的参数中指定的 URL 时发生。攻击者利用此漏洞将用户重定向到恶意站点。

在提供的 PHP 代码片段中

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController 
{

    #[Route('/dynamic_redirect', methods: ['GET'])]
    public function dynamicRedirect(#[MapQueryParameter] string $url): Response 
    {
        return $this->redirect($url);
    }
}

控制器函数根据 url 查询参数重定向用户,但没有进行适当的验证。攻击者可以制作恶意 URL,将毫无戒心的用户引导到恶意网站。为了防止开放重定向,请务必在重定向之前验证和清理用户输入,并避免在重定向函数中直接使用不可信的输入。

文件上传漏洞

文件上传漏洞是指应用程序未能正确验证和处理文件上传时出现的问题。确保安全处理文件上传以防止各种类型的攻击至关重要。以下是一些在 Symfony 中帮助缓解此问题的一般准则

验证文件类型和大小

始终在服务器端验证文件类型,以确保只接受允许的文件类型。此外,考虑限制上传文件的大小,以防止拒绝服务攻击,并确保您的服务器有足够的资源来处理上传。

PHP 属性示例

use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Validator\Constraints\File;

class UploadDto
{
    public function __construct(
        #[File(
            maxSize: '1024k',
            mimeTypes: [
                'application/pdf',
                'application/x-pdf',
            ],
        )]
        public readonly UploadedFile $file,
    ){}
}

Symfony 表单示例

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints\File;

class FileForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'constraints' => [
                    new File([
                        'maxSize' => '1024k', 
                        'mimeTypes' => [
                            'application/pdf',
                            'application/x-pdf',
                        ],
                    ]),
                ],
            ]);
    }
}

使用唯一的文件名

确保每个上传的文件都具有唯一的名称,以防止覆盖现有文件。您可以使用唯一标识符和原始文件名的组合来生成唯一的名称。

安全地存储上传文件

将上传文件存储在公共目录之外,以防止直接访问。如果您使用公共目录存储它们,请配置您的 Web 服务器以拒绝访问上传目录。

参考 文件上传备忘单 了解更多信息。

目录遍历

目录或路径遍历攻击旨在通过操纵包含“../”点-点-斜杠序列及其变体或使用绝对文件路径的文件引用输入数据,来访问存储在服务器上的文件和目录。更多详细信息请参考 OWASP 路径遍历

您可以通过验证所请求文件位置的绝对路径是否正确,或从文件名输入中剥离目录信息来保护您的应用程序免受目录遍历攻击。

  • 使用 PHP 的 realpath 函数检查路径是否存在,并确保它指向存储目录
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Annotation\Route;

class ExampleController extends AbstractController 
{

    #[Route('/download', methods: ['GET'])]
    public function download(#[MapQueryParameter] string $filename): Response 
    {
        $storagePath = $this->getParameter('kernel.project_dir') . '/storage';
        $filePath = $storagePath . '/' . $filename;

        $realBase = realpath($storagePath);
        $realPath = realpath($filePath);

        if ($realPath === false || !str_starts_with($realPath, $realBase))
        {
            //Directory Traversal!
        }

        // ...

    }
}
  • 使用 PHP 的 basename 函数剥离目录信息
// ...

$storagePath = $this->getParameter('kernel.project_dir') . '/storage';
$filePath = $storagePath . '/' . basename($filename);

// ...

依赖项漏洞

依赖项漏洞可能使您的应用程序面临各种风险,因此采用最佳实践至关重要。请保持所有 Symfony 组件和第三方库的最新状态。

Composer,PHP 的依赖管理器,使更新 PHP 包变得容易

composer update

当使用多个依赖项时,其中一些可能包含安全漏洞。为了解决这个问题,Symfony 提供了 Symfony 安全检查器。此工具专门检查您项目中的 composer.lock 文件,以识别已安装依赖项中的任何已知安全漏洞,并解决您的 Symfony 项目中任何潜在的安全问题。

要使用安全检查器,请使用 Symfony CLI 运行以下命令

symfony check:security

您还应该考虑类似的工具

跨域资源共享 (CORS)

CORS 是 Web 浏览器中实现的一项安全功能,用于控制一个域中的 Web 应用程序如何请求和与托管在其他域上的资源进行交互。

在 Symfony 中,您可以使用 nelmio/cors-bundle 管理 CORS 策略。此 Bundle 让您可以在不更改服务器设置的情况下精确控制 CORS 规则。

要使用 Composer 安装它,请运行

composer require nelmio/cors-bundle

对于 Symfony Flex 用户,安装会自动在 config/packages 目录中生成一个基本配置文件。查看以 /API 前缀开头的路由的配置示例。

# config/packages/nelmio_cors.yaml
nelmio_cors:
    defaults:
        origin_regex: true
        allow_origin: ['*']
        allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
        allow_headers: ['*']
        expose_headers: ['Link']
        max_age: 3600
    paths:
        '^/api': ~  # ~ means that configurations for this path is inherited from defaults

建议通过在响应中添加必要的安全头来增强 Symfony 应用程序的安全性,例如

  • Strict-Transport-Security
  • X-Frame-Options
  • X-Content-Type-Options
  • 内容安全策略
  • X-Permitted-Cross-Domain-Policies
  • Referrer-Policy
  • Clear-Site-Data
  • Cross-Origin-Embedder-Policy
  • Cross-Origin-Opener-Policy
  • Cross-Origin-Resource-Policy
  • Cache-Control

要了解有关各个头的更多详细信息,请参考 OWASP 安全头项目

在 Symfony 中,您可以手动添加这些头,或者通过监听 ResponseEvent 将它们自动添加到每个响应中,或者配置 Nginx 或 Apache 等 Web 服务器。

use Symfony\Component\HttpFoundation\Request;

$response = new Response();
$response->headers->set('X-Frame-Options', 'SAMEORIGIN');

会话和Cookie管理

默认情况下,会话已安全配置并启用。但是,它们可以在 config/packages/framework.yaml 文件中的 framework.session 键下手动控制。请确保在您的会话配置中设置以下内容,以使您的应用程序更具安全性意识。

确保 cookie_secure 没有显式设置为 false(它默认设置为 true)。将 `http_only` 设置为 true 意味着 Cookie 将无法被 JavaScript 访问。

cookie_httponly: true

务必设置较短的会话 TTL(生存时间)持续时间。根据 OWASP 的建议,高价值应用程序的会话 TTL 目标为 2-5 分钟,低风险应用程序为 15-30 分钟。

cookie_lifetime: 5

建议将 cookie_samesite 设置为 laxstrict,以防止 Cookie 从跨域请求发送。lax 允许 Cookie 与“安全”的顶级导航和同站点请求一起发送。使用 strict 时,如果 HTTP 请求不是来自同一域,则无法发送任何 Cookie。

cookie_samesite: lax|strict

cookie_secure 设置为 auto 可确保 Cookie 仅通过安全连接发送,这意味着 HTTPS 为 true,HTTP 协议为 false

cookie_secure: auto

OWASP 在 会话管理备忘单 中提供了更多关于会话的通用信息。您还可以参考 Cookie 安全指南


在 Symfony 中,会话由框架自身管理,并依赖于 Symfony 的会话处理机制,而不是通过 php.ini 文件中的 session.auto_start = 1 指令进行 PHP 的默认会话处理。PHP 中的 session.auto_start = 1 指令用于在每次请求时自动启动会话,从而绕过对 session_start() 的显式调用。但是,当使用 Symfony 进行会话管理时,建议禁用 session.auto_start 以防止冲突和意外行为。

认证

Symfony 安全 提供了一个强大的身份验证系统,包括提供者(providers)、防火墙(firewalls)和访问控制(access controls),以确保安全和受控的访问环境。身份验证设置可以在 config/packages/security.yaml 中配置。

  • 提供者

    Symfony 身份验证依赖于提供者从各种存储类型(如数据库、LDAP 或自定义源)获取用户信息。提供者根据定义的属性获取用户并加载相应的用户对象。

    下面的示例展示了 实体用户提供者,它使用 Doctrine 通过唯一标识符获取用户。

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    
  • 防火墙

    Symfony 使用防火墙为应用程序的不同部分定义安全配置。每个防火墙定义了一组针对传入请求的特定规则和操作。它们通过指定哪些路由或 URL 是安全的、要使用的身份验证机制以及如何处理未经授权的访问来保护应用程序的不同部分。防火墙可以与特定的模式、请求方法、访问控制和身份验证提供者相关联。

    firewalls:
        dev: # disable security on routes used in development env
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin: # handle authentication in /admin pattern routes
            lazy: true
            provider: app_user_provider
            pattern: ^/admin
            custom_authenticator: App\Security\AdminAuthenticator
            logout:
                path: app_logout
                target: app_login
        main: # main firewall that include all remaining routes
            lazy: true
            provider: app_user_provider
    
  • 访问控制

    访问控制决定哪些用户可以访问应用程序的特定部分。这些规则由路径模式和所需的角色或权限组成。访问控制规则在 access_control 键下配置。

    access_control:
        - { path: ^/admin, roles: ROLE_ADMIN } # only user with ROLE_ADMIN role is allowed
        - { path: ^/login, roles: PUBLIC_ACCESS } # everyone can access this route
    

错误处理披露

Symfony 拥有强大的错误处理系统。默认情况下,出于安全原因,Symfony 应用程序配置为仅在开发环境中显示详细的错误消息。在生产环境中,则显示一个通用错误页面。Symfony 的错误处理系统还允许根据不同的 HTTP 状态码定制错误页面,提供无缝且品牌化的用户体验。此外,Symfony 记录详细的错误信息,帮助开发人员高效地识别和解决问题。

有关与 Symfony 无关的错误处理信息,请参考 错误处理备忘单

敏感数据

在 Symfony 中,存储 API 密钥等配置的最佳方式是使用环境变量,这些变量依赖于应用程序的位置。为了确保敏感值的安全性,Symfony 提供了一个 秘密管理系统,其中值使用加密密钥额外编码并存储为 秘密

考虑一个将 API_KEY 存储为秘密的示例

要生成一对加密密钥,您可以运行以下命令。私钥文件高度敏感,不应提交到代码仓库中。

bin/console secrets:generate-keys

此命令将在 config/secrets/env(dev|prod|etc.) 中为 API_KEY 秘密生成一个文件

bin/console secret:set API_KEY

您可以像访问环境变量一样在代码中访问秘密值。非常重要的一点是,如果存在同名的环境变量和秘密,环境变量中的值将始终覆盖秘密

更多详细信息请参考 Symfony 秘密文档

总结

  • 确保您的应用程序在生产环境中未处于调试模式。要关闭调试模式,请将您的 APP_ENV 环境变量设置为 prod

    APP_ENV=prod
    
  • 确保您的 PHP 配置是安全的。有关安全 PHP 配置设置的更多信息,您可以参考 PHP 配置备忘单

  • 确保您的 Web 服务器中 SSL 证书配置正确,并将其配置为通过将 HTTP 流量重定向到 HTTPS 来强制使用 HTTPS。

  • 实施安全头以增强应用程序的安全态势。

  • 确保文件和目录权限设置正确,以最大限度地降低安全风险。

  • 定期备份您的生产数据库和关键文件。制定恢复计划,以便在出现任何问题时快速恢复您的应用程序。

  • 使用安全检查器扫描您的依赖项,以识别已知漏洞。

  • 考虑设置监控工具和错误报告机制,以便在生产环境中快速识别和解决问题。探索诸如 Blackfire.io 之类的工具。

参考资料