diff --git a/app/config/eccube/packages/twig_extensions.yaml b/app/config/eccube/packages/twig_extensions.yaml index 3ab3be512b3..6b713dd5d22 100644 --- a/app/config/eccube/packages/twig_extensions.yaml +++ b/app/config/eccube/packages/twig_extensions.yaml @@ -8,3 +8,166 @@ services: #Twig\Extensions\DateExtension: ~ # Twig\Extensions\IntlExtension: ~ #Twig\Extensions\TextExtension: ~ + + eccube.twig_sandbox.policy: + class: Twig\Sandbox\SecurityPolicy + arguments: + $allowedTags: "%eccube.twig_sandbox.allowed_tags%" + $allowedFilters: "%eccube.twig_sandbox.allowed_filters%" + $allowedFunctions: "%eccube.twig_sandbox.allowed_functions%" + $allowedMethods: "%eccube.twig_sandbox.allowed_methods%" + $allowedProperties: "%eccube.twig_sandbox.allowed_properties%" + eccube.twig_sandbox.extension: + class: Twig\Extension\SandboxExtension + arguments: + - '@eccube.twig_sandbox.policy' + - false + tags: ['twig.extension'] + Eccube\Twig\Sandbox\SecurityPolicyDecorator: + decorates: 'eccube.twig_sandbox.policy' +parameters: + eccube.twig_sandbox.allowed_tags: + - 'apply' + - 'block' + - 'deprecated' + - 'embed' + - 'extends' + - 'flush' + - 'for' + - 'if' + - 'set' + - 'spaceless' + - 'verbatim' + - 'with' + - 'form_theme' + - 'stopwatch' + - 'trans' + - 'trans_default_domain' + eccube.twig_sandbox.allowed_filters: + - 'abs' + - 'batch' + - 'capitalize' + - 'column' + - 'convert_encoding' + - 'country_name' + - 'currency_name' + - 'currency_symbol' + - 'date' + - 'date_modify' + - 'default' + - 'escape' + - 'first' + - 'format' + - 'format_currency' + - 'format_date' + - 'format_datetime' + - 'format_number' + - 'format_time' + - 'join' + - 'json_encode' + - 'keys' + - 'language_name' + - 'last' + - 'length' + - 'locale_name' + - 'lower' + - 'merge' + - 'nl2br' + - 'number_format' + - 'replace' + - 'reverse' + - 'round' + - 'slice' + - 'spaceless' + - 'split' + - 'striptags' + - 'timezone_name' + - 'title' + - 'trim' + - 'upper' + - 'url_encode' + - 'abbr_class' + - 'abbr_method' + - 'file_link' + - 'file_relative' + - 'format_args' + - 'format_args_as_text' + - 'humanize' + - 'serialize' + - 'trans' + - 'yaml_dump' + - 'yaml_encode' + - 'currency_symbol' + - 'date_day' + - 'date_day_with_weekday' + - 'date_format' + - 'date_min' + - 'date_sec' + - 'doctrine_format_sql' + - 'doctrine_prettify_sql' + - 'doctrine_pretty_query' + - 'doctrine_replace_query_parameters' + - 'e' + - 'ellipsis' + - 'file_ext_icon' + - 'form_encode_currency' + - 'format_*_number' + - 'format_log_message' + - 'no_image_product' + - 'price' + - 'purify' + - 'time_ago' + eccube.twig_sandbox.allowed_functions: + - 'cycle' + - 'date' + - 'max' + - 'min' + - 'random' + - 'range' + - 'country_timezones' + - 'absolute_url' + - 'asset' + - 'asset_version' + - 'csrf_token' + - 'form_parent' + - 'fragment_uri' + - 'impersonation_exit_path' + - 'impersonation_exit_url' + - 'is_granted' + - 'logout_path' + - 'logout_url' + - 'path' + - 'relative_path' + - 't' + - 'url' + - 'active_menus' + - 'class_categories_as_json' + - 'country_names' + - 'csrf_token_for_anchor' + - 'currency_names' + - 'currency_symbol' + - 'field_choices' + - 'field_errors' + - 'field_help' + - 'field_label' + - 'field_name' + - 'field_value' + - 'get_all_carts' + - 'get_cart' + - 'get_carts_total_price' + - 'get_carts_total_quantity' + - 'has_errors' + - 'is_reduced_tax_rate' + - 'language_names' + - 'product' + - 'workflow_can' + - 'workflow_has_marked_place' + - 'workflow_marked_places' + - 'workflow_metadata' + - 'workflow_transition' + - 'workflow_transition_blockers' + - 'workflow_transitions' + eccube.twig_sandbox.allowed_methods: + 'Symfony\Bridge\Twig\AppVariable': [ 'getrequest' ] + 'Symfony\Component\HttpFoundation\Request': [ 'geturi' ] + eccube.twig_sandbox.allowed_properties: [] \ No newline at end of file diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 213da6dad2a..b44c21c8885 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,2 +1,6 @@ parameters: level: 1 + ignoreErrors: + - + message: "#^Function twig_include not found\\.$#" + path: src/Eccube/Twig/Extension/IgnoreTwigSandboxErrorExtension.php \ No newline at end of file diff --git a/src/Eccube/Resource/template/default/Product/detail.twig b/src/Eccube/Resource/template/default/Product/detail.twig index 876ebc2233c..543d67beb31 100755 --- a/src/Eccube/Resource/template/default/Product/detail.twig +++ b/src/Eccube/Resource/template/default/Product/detail.twig @@ -439,7 +439,7 @@ file that was distributed with this source code. {% if Product.freearea %}
- {{ include(template_from_string(Product.freearea)) }} + {{ include(template_from_string(Product.freearea), sandboxed = true) }}
{% endif %} diff --git a/src/Eccube/Resource/template/default/default_frame.twig b/src/Eccube/Resource/template/default/default_frame.twig index e1278a0a695..51d3fa35ed1 100644 --- a/src/Eccube/Resource/template/default/default_frame.twig +++ b/src/Eccube/Resource/template/default/default_frame.twig @@ -16,7 +16,7 @@ file that was distributed with this source code. {{ BaseInfo.shop_name }}{% if subtitle is defined and subtitle is not empty %} / {{ subtitle }}{% elseif title is defined and title is not empty %} / {{ title }}{% endif %} {% if Page.meta_tags is not empty %} - {{ include(template_from_string(Page.meta_tags)) }} + {{ include(template_from_string(Page.meta_tags), sandboxed = true) }} {% if Page.description is not empty %} {% endif %} diff --git a/src/Eccube/Twig/Extension/IgnoreTwigSandboxErrorExtension.php b/src/Eccube/Twig/Extension/IgnoreTwigSandboxErrorExtension.php new file mode 100644 index 00000000000..32b928c5857 --- /dev/null +++ b/src/Eccube/Twig/Extension/IgnoreTwigSandboxErrorExtension.php @@ -0,0 +1,78 @@ + true, 'needs_context' => true, 'is_safe' => ['all']]), + ]; + } + + /** + * twig sandboxの例外を操作します + * app_env = devの場合、エラーを表示する + * app_env = prodの場合、エラーを表示しない + * + * @param Environment $env + * @param $context + * @param $template + * @param $variables + * @param $withContext + * @param $ignoreMissing + * @param $sandboxed + * + * @return string|void + * + * @throws LoaderError + * @throws SecurityError + */ + public function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + try { + return \twig_include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); + } catch (SecurityError $e) { + + // devではエラー画面が表示されるようにする + $appEnv = env('APP_ENV'); + if ($appEnv === 'dev') { + throw $e; + } else { + // ログ出力 + log_warning($e->getMessage(), ['exception' => $e]); + + // 例外がスローされた場合、sandboxが効いた状態になってしまうため追加 + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$sandbox->isSandboxedGlobally()) { + $sandbox->disableSandbox(); + } + } + } + } +} diff --git a/src/Eccube/Twig/Sandbox/SecurityPolicyDecorator.php b/src/Eccube/Twig/Sandbox/SecurityPolicyDecorator.php new file mode 100644 index 00000000000..8615449624b --- /dev/null +++ b/src/Eccube/Twig/Sandbox/SecurityPolicyDecorator.php @@ -0,0 +1,47 @@ +securityPolicy = $securityPolicy; + } + + public function checkSecurity($tags, $filters, $functions) + { + $this->securityPolicy->checkSecurity($tags, $filters, $functions); + } + + public function checkMethodAllowed($obj, $method) + { + // __toStringの場合はチェックをスキップする + if ($method === '__toString') { + return; + } + $this->securityPolicy->checkMethodAllowed($obj, $method); + } + + public function checkPropertyAllowed($obj, $method) + { + $this->securityPolicy->checkPropertyAllowed($obj, $method); + } +} \ No newline at end of file diff --git a/tests/Eccube/Tests/Twig/Extension/IgnoreTwigSandboxErrorExtensionTest.php b/tests/Eccube/Tests/Twig/Extension/IgnoreTwigSandboxErrorExtensionTest.php new file mode 100644 index 00000000000..cac1c1b2fc8 --- /dev/null +++ b/tests/Eccube/Tests/Twig/Extension/IgnoreTwigSandboxErrorExtensionTest.php @@ -0,0 +1,109 @@ +createProduct(); + $Product->setFreeArea('__RENDERED__'.$snippet); + $this->entityManager->flush(); + + $crawler = $this->client->request('GET', $this->generateUrl('product_detail', ['id' => $Product->getId()])); + $text = $crawler->text(); + + // $snippetがsandboxで制限された場合はフリーエリアは空で出力されるため、__RENDERED__の出力有無で結果を確認する + self::assertStringContainsString($whitelisted ? '__RENDERED__' : '', $text); + } + + /** + * @dataProvider twigSnippetsProvider + * @dataProvider twigVarMetaTagsProvider + */ + public function testMetatags($snippet, $whitelisted) + { + $Page = $this->entityManager->getRepository(Page::class)->find(1); + $Page->setMetaTags('__RENDERED__'.$snippet); + $this->entityManager->flush(); + + $crawler = $this->client->request('GET', $this->generateUrl($Page->getUrl())); + $text = $crawler->text(); + + // ホワイトリストに入っている場合__RENDERED__が表示される + if ($whitelisted) { + self::assertStringContainsString('__RENDERED__', $text); + } else { + self::assertStringNotContainsString('__RENDERED__', $text); + } + // 入力可能ではない値の場合は、システムエラーが発生する + self::assertStringNotContainsString('システムエラーが発生しました', $text); + + } + + public function twigSnippetsProvider() + { + // 0: twigスニペット, 1: ホワイトリスト対象かどうか + return [ + ['{% set foo = "bar" %}', true], + ['{% spaceless %}
test
{% endspaceless %}', true], + ['{% flush %}', true], + ['{% apply lower|escape("html") %}SOME TEXT{% endapply %}', true], + ['{% macro input(name, value, type = "text", size = 20) %}{% endmacro %}', false], + ['{% sandbox %}{% include "user.html" %}{% endsandbox %}', false], + ['{{ "-5"|abs }}', true], + ['{{ "2020/02/01"|date_modify("+1 day")|date("m/d/Y") }}', true], + ['{{ [1, 2, 3, 4]|first }}', true], + ['{{ file|format_file(line, text = null) }}', false], + ['{{ [1, 2, 3]|reduce((carry, v) => carry + v) }}', false], + ['{{ "

test

" |raw }}', false], + ['{{ url("homepage") }}', true], + ['{{ random(1, 100) }}', true], + ['{% for i in range(3, 0) %} {{ i }}, {% endfor %}', true], + ['{{ dump(9) }}', false], + ['{{ constant("RSS", date) }}', false], + ['{{ include(template_from_string("Hello")) }}', false], + ['{{ Product.main_list_image|no_image_product }}', true], + ]; + } + + public function twigVarFreeAreaProvider() + { + // 0: twigスニペット, 1: ホワイトリスト対象かどうか + return [ + ['{{ app.user }}', false], + ['{{ Product.name }}', true], + ['{{ app.request.uri }}', true], + ['{{ app.request.getUri }}', true], + ]; + } + + public function twigVarMetaTagsProvider() + { + // 0: twigスニペット, 1: ホワイトリスト対象かどうか + return [ + ['{{ app.debug }}', false], + ['{{ BaseInfo.shop_name }}', true], + ['{{ app.request.uri }}', true], + ['{{ app.request.getUri }}', true], + ]; + } +}