在 PHP 中实现文件路径名(编辑模板代码时,可新增文件)的安全验证

1、查看请求参数:asset[key]: assets/2022/02/25.css,准备在模板文件中新增文件:assets/2022/02/25.css。响应失败:Theme files may not be stored in subfolders。如图1

图1

2、查看请求参数:asset[key]: assets/25.css,准备在模板文件中新增文件:assets/25.css。响应成功。如图2

图2

3、查看请求参数:asset[key]: assets/02\25.css,准备在模板文件中新增文件:assets/02\25.css。响应失败:”assets/02\\25.css” contains illegal characters。如图3

图3

4、查看请求参数:asset[key]: templates/customers/order.order1.liquid,准备在模板文件中新增文件:templates/customers/order.order1.liquid。响应成功。目录:templates/customers 早已经生成好。如图4

图4

5、最终整理出的验证规则如下:
(1)路径参数必须以已经生成好的目录开头。目录包含:[‘layouts/’, ‘auth/’, ‘pages/’, ‘components/’, ‘assets/’, ‘js/’, ‘sass/’, ‘assets/iconfont/’, ‘js/common/’, ‘js/view/’, ‘js/view/account/’, ‘js/view/cart/’, ‘js/view/collections/’, ‘js/view/product/’, ‘js/view/productlist/’, ‘js/view/search/’]
(2)当 asset[key] 去掉目录开头部分后,便仅需要验证纯粹的文件名,不允许再包含 / 与 \
(3)验证 asset[key] 的文件扩展名,其文件后缀名必须属于一个预先定义的数组中,扩展名包含:[‘.php’, ‘.json’, ‘.css’, ‘.scss’, ‘.js’, ‘.vue’]

6、难点主要在于第 2 条验证规则的实现,参考:https://stackoverflow.com/questions/31089394/check-if-string-is-valid-filename 。POSIX“完全可移植的文件名”列出了这些:A-Z a-z 0-9 . _ – 。新建文件:FilePathValidator.php,不允许空格。

<?php
function validate($filename) {
 if (preg_match('/^[\w\-.]+$/', $filename)) { 
  return $filename . ' 验证成功';
 } else {
  return $filename . ' 验证失败';
 }
}

$filenames = ['assets/2022/02/25.css', 'assets/02\25.css', '/25.css', '25.css/', '2_- 5.css', '2_-5.css', '中国.css', 'testjpg.', '.testjpg', 'test.jpg'];
foreach ($filenames as $filename) {
 echo validate($filename) . PHP_EOL;
}
?>


7、查看运行结果,验证成功的 3 个文件名中,需要再排除掉:testjpg.、.testjpg,而第 3 条验证规则恰好可以排队掉:testjpg.、.testjpg。目录开头与文件扩展名结尾,可以使用 dirname() 与 Laravel 中的验证方法:ends_with:foo,bar,… 验证的字段必须以给定的值之一结尾。如图5

图5

assets/2022/02/25.css 验证失败
assets/02\25.css 验证失败
/25.css 验证失败
25.css/ 验证失败
2_- 5.css 验证失败
2_-5.css 验证成功
中国.css 验证失败
testjpg. 验证成功
.testjpg 验证成功
test.jpg 验证成功
    /**
     * Return the validation rules.
     *
     * @return array<string, array<mixed>>
     */    public function rules(): array
    {
        $extensions = implode(",", ThemeAsset::BLADE_EXTENSIONS);
        return [
            'key' => [
                // 当获取文件路径中的目录部分后,其必须属于目录列表之一
                function ($attribute, $value, $fail) {
                    if (!in_array(dirname($value) . '/', ThemeAsset::BLADE_DIRS)) {
                        $fail('The directory portion of ' . $attribute . ' can only contain the following: ' . implode(", ", ThemeAsset::BLADE_DIRS));
                    }
                },
                // 当获取文件路径中的文件名部分后,便仅需要验证纯粹的文件名,只允许包含:A-Z、a-z、0-9、.、_、-
                function ($attribute, $value, $fail) {
                    if (!preg_match('/^[\w\-.]+$/', basename($value))) {
                        $fail('The file name portion of ' . $attribute . ' can only contain the following: A-Z, a-z, 0-9, ., _, -');
                    }
                },
                // 验证的字段必须以给定的值之一结尾
                'ends_with:' . $extensions,
            ],
        ];
    }

8、这会过滤掉不属于 ISO-1252 的有效文件名,例如日文、中文、西欧、西里尔文、中东……字符。 此外,它不会过滤掉具有有效字符但在操作系统中无效的文件名,例如 COM1、LPT1、AUX …。此二项缺陷暂时皆可以接受。后续可能会解决不会过滤掉具有有效字符但在操作系统中无效的文件名。如图6

图6

永夜