在PHP中保护路径

5
我正在编写一些PHP代码,需要使用不同内容目录的路径,并在稍后包含页面的各个部分。我正在尝试确保这些路径是正确的,并且没有违反应用程序的规则。这些规则如下:
  1. PRIVATEDIR(相对于DOCUMENT_ROOT定义)必须在DOCUMENT_ROOT之上。
  2. CONTENTDIR(相对于PRIVATEDIR定义)必须位于PRIVATEDIR之下,并且不能返回到DOCUMENT_ROOT
  3. 其余的*DIRS(相对于CONTENTDIR定义)必须位于CONTENTDIR之下。
我在单例控制器类中设置了一些默认值,然后用户将要覆盖的路径数组传递给此类的构造函数。然后,我想检查它们是否符合上述规则。以下是我开始处理的方法... 注意:请注意我在下面的代码中使用的error_reporting,请不要重复此操作!我误解了该命令的工作方式。如果您想知道原因,请参见评论中stealthyninja和Col. Shrapnel的备注(感谢他们向我指出)。
private $opts = array( // defaults
   'PRIVATEDIR'   => '..',        // relative to document root
   'CONTENTDIR'   => 'content',   // relative to private dir
   ...
   ...
);

private function __construct($options) { //$options is the user defined options
    error_reporting(0);
    if(is_array($options)) {
        $this->opts = array_merge($this->opts, $options);
    }

    if($this->opts['STATUS']==='debug') {
        error_reporting(E_ALL | E_NOTICE | E_STRICT);
    }

    $this->opts['PUBLICDIR']  = realpath($_SERVER['DOCUMENT_ROOT'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['PRIVATEDIR'] = realpath($this->opts['PUBLICDIR']
                                        .$this->opts['PRIVATEDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['CONTENTDIR'] = realpath($this->opts['PRIVATEDIR']
                                        .$this->opts['CONTENTDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['CACHEDIR']   = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['CACHEDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['ERRORDIR']   = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['ERRORDIR'])
                                        .DIRECTORY_SEPARATOR;
    $this->opts['TEMPLATEDIR' = realpath($this->opts['CONTENTDIR']
                                        .$this->opts['TEMPLATEDIR'])
                                        .DIRECTORY_SEPARATOR;

   ...
   ...
   ...


    // then here I have to check that PRIVATEDIR is above PUBLICDIR
    // and that all the rest remain within private dir and don't drop 
    // down into (or below) PUBLICDIR again. And die with an error if
    // they don't conform.
}

事实上,这似乎是一项很大的工作,特别是每次访问页面之前必须运行它,然后才能进行其他操作(例如检查我正在提供的页面的缓存版本),因为路径基本上是静态的。
我想,网站的维护者(目前是我)应该知道他们提供的路径,并且应该自己检查它们是否符合规则。但是,我认为更明智的是,我应该对检查路径是否符合规则也同样负责,因为意外总会发生(特别是在网站不断增长/更改/有新的维护者的情况下...),在安全关键环境中如果可以捕获到,则没有理由不去捕获。
因此,我已经基本上决定要检查这些路径,问题是我的方法有多好?它似乎是一个次优解。您如何改进它或建议什么替代方案?是否有办法解决每次页面加载时都需要检查的问题,以及某种方式只在配置更改时进行检查?(那将是理想的,但我认为不容易实现。)
谢谢。

1
你的错误报告简直荒谬。将其改为error_reporting(E_ALL | E_STRICT);,然后永远不要再动它了。 - Your Common Sense
你要么看了白痴写的文章,要么误解了它们。在生产服务器上,你应该关闭 display_errors 设置,并开启 log_errors。而 error_reporting 应始终保持最大并保持完整。我还很好奇,你的代码中是否有任何 die() 指令? - Your Common Sense
1
@stealthyninja,@Col. Shrapnel。啊,我没有意识到我正在关闭错误报告到日志。那不是我想要或打算做的。谢谢你指出来。我认为这可能是一些白痴和我的一些误解的混合(还是只有我是个白痴?)。我知道我已经读过像我所做的那样关闭error_reporting的代码。但显然它并没有做我以为它会做的事情。 - tjm
2
我在想,现在的礼仪是什么,我应该改变我帖子中的代码吗?我不希望人们只看到代码(而不是阅读这些评论)并无意中传播病毒。话虽如此,如果我改变了它,这些评论将没有太多意义。 - tjm
请注意,error_reporting(E_ALL | E_NOTICE | E_STRICT)error_reporting(E_ALL | E_STRICT)相同,因为E_ALL已经包括除E_STRICT之外的所有内容。 (请参见文档 - Tgr
显示剩余3条评论
3个回答

1

使用strpos查找子字符串如何?这不是最优的解决方案,但如果您只想为在网站上已有很大权力的人提供基本保护,那么这是我会这样做的:

// PRIVATEDIR already includes PUBLICDIR at this point
if(strpos($this->opts['PRIVATEDIR'],$this->opts['PUBLICDIR']) !== 0) {
die('ERROR: '.$this->opts['PRIVATEDIR'].' must be located in .'$this->opts['PUBLICDIR']);
}

请注意三重不等号,这确保了PUBLICDIR出现在PRIVATEDIR字符串的位置0处,而不是仅仅未找到或出现在字符串的其他位置。此外,您应该检查它们是否相同(PUBLICDIR!= PRIVATEDIR),因为您可以将其设置为“./test /.. /”,这将使您返回到“。”。

谢谢。但我得先处理所有的realpath,对吧?因为当路径被提供时,它们是相对于彼此的。 - tjm
确切地说,一旦它们经过realpath函数,它们应该是完整的规范路径 - 在realpath之前,它们是非常危险的!一旦它们都以完整路径形式存在,您可以比较它们并确保publicdir子字符串是privatedir包含的第一件事。 - Michael Petrov

1

我会让网站管理员决定目录的位置,并提供上述方便的默认值。有人可能希望为服务器上的所有站点使用一个常见的错误或临时目录,或者将私有目录放在公共目录中并通过 .htaccess 进行保护(这是不好的做法,但在一些廉价的 Web 主机上可能是必要的)。

此外,在构造函数中进行数组合并有什么意义呢?如果 $opts 数组在那个时候之前无法获得值,那么对于阅读代码的人来说会很困惑,并且在启用 E_STRICT 时会出现错误;如果它有一个默认值或其他什么东西,那么像你所做的那样更改它是具有误导性的(如果有人在你的类定义中看到 $opts = array('TEMPLATEDIR'=>'/templates'),然后在代码中找到对 $config->$opts['TEMPLATEDIR'] 的引用,他会期望它是 /templates);你应该要么在选项中一开始就使用完整路径,要么将完整路径保存在另一个变量中。


感谢您抽出时间回复,但是当我将其声明为 private $opts = array( ... ); 时,$opts 数组已经被填充了,这些是默认设置,然后在构造函数中,我将数组与用户定义的设置合并。我想要检查这些设置以确保安全性。我认为我需要更新/重新表述问题,因为可能我没有表达清楚。如果是这样,请原谅。 - tjm
当然,你说得对,如果我在同一个数组中将路径从相对路径更改为绝对路径可能会引起混淆。我考虑过这个问题,但没有深入思考。我想我可能会采纳你的建议。关于为服务器使用一个常见的错误目录也是一个好主意,我会考虑一下。至于将私有目录放在公共目录中,我最初也考虑了这种可能性,并在代码中添加了一些重要的注释来确保通过.htaccess进行阻止,但最终我决定不支持它。 - tjm

0

如果目标是确保代码留在正确的目录中查找文件,为什么要使用realpath()而不检查您是否仍在受限制的目录内?或者您不关心这些路径中是否包含“../”?

我会推断至少CACHEDIR是可写的 - 但位于文档根目录内 - 从安全角度来看,这不是一个好主意。

此外,您正在重写现有变量,而不是从模板值构建新变量 - 即使您在构造函数中这样做,对于应该在声明时立即成为最终值的值来说,这是一种相当混乱的方法。


感谢symcbean的回答,这正是我担心的问题;路径包含..PRIVATEDIR变成..,然后CONTENTDIR变成public_html(或等效)时,将其带回公共区域,而不应该这样做(对于任何其他“突破”也是如此)。我将在我放置注释// then here I have to check ...的地方进行检查,在示例中我只是没有输入所有的检查代码,以免使示例过长。如果这让事情变得混乱,我很抱歉。还有一点,所有的realpath和检查代码,似乎需要测试很多。 - tjm
每次加载页面时,这些路径在实际上都是静态的。至于重写现有变量,我只是在内部覆盖了我的类中设置的默认值($this->opts),而不是传递进来的数组。这样做是否仍然是不好的做法? - tjm
抱歉,我刚刚重新阅读了你的帖子,一开始可能会对你关于覆盖数组的说法感到困惑。我认为你是指Tgr在他的回答中提到的同样的问题。在这种情况下,你是完全正确的,对于我有点慢反应,我感到很抱歉。 - tjm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接