介绍
Laravel,Symfony和其他现代PHP框架使用环境变量来存储从一台机器更改为下一台机器的安全凭证和配置。
最新的Laravel版本对环境变量的加载方式做了一些小改动。这一变化最终破坏了第三方图书馆和Laravel本身。
随后的讨论清楚地表明,许多开发人员(包括我自己)都没有意识到PHP中的复杂环境变量实际上是多少。有很多方法可以阅读它们,因为它们可以编写它们而且没有一个选项是万无一失的。
让我们分解环境变量,它们如何工作以及如何在代码中正确使用它们。
什么是环境变量?
自20世纪80年代以来,每个流行的操作系统都支持变量,就像编程语言一样。当进程启动时,它继承父进程的变量。该过程使用这些变量来发现有关其运行环境的信息,例如保存临时文件的首选位置或用户主目录的位置。
如果您使用的是Unix操作系统,如MacOS或Linux,您可以打开终端并查看$HOME
环境变量的值,如下所示:
» echo $HOME
/Users/matt
如果您使用的是Windows,则可以打开Powershell并输入以下内容:
Write-Output $env:HOMEPATH
通常,环境变量以大写形式写入,下划线分隔单词LIKE_THIS
。
对应用程序配置使用环境变量
在十二因子应用方法推广使用环境变量来存储配置软件的想法。从那以后,它成为事实上的标准,拥有来自Web框架,云提供商以及您用于构建软件的任何其他内容的一流支持。
有一些主要的缺点,所以如果你还没有,请在采用它们之前进行研究。如果您已经在使用它们,请继续阅读以了解如何安全地使用它们。
设置环境变量
我们将讨论如何设置环境变量,以便应用程序可以访问它。
CLI
shell中设置的任何环境变量都可用于您启动的任何进程。例如,您已经可以访问HOME
我们在上面发现的变量:
» php -r 'var_dump(getenv("HOME"));'
string(11) "/Users/matt"
但是,您可能希望添加自己的变量。有很多方法可以做到这一点。最简单的方法是在运行命令之前声明环境变量:
» APP_ENV=local php -r 'var_dump(getenv("APP_ENV"));'
string(5) "local"
它不会持久存在,因此每次运行命令时都需要添加它。当您添加更多环境变量时,这很快就会变得烦人。你真的不想在生产中使用这种技术,但它对于快速测试某些东西很方便。
Unix系统的另一个有用技巧是使用该export
命令。导出环境变量后,它将在所有后续命令中可用,直到退出shell。
» export APP_ENV=local
» php -r 'var_dump(getenv("APP_ENV"));'
string(5) "local"
永久设置环境变量还有很多其他选项,但它们并不真正用于保密,通常需要以纯文本形式存储环境变量。
卷筒纸
当我们的Web服务器处理请求时,我们不会自己启动该过程。相反,PHP-FPM产生了这个过程。
第一个选项是从PHP-FPM传递环境变量。默认情况下,PHP-FPM在启动PHP进程之前清除现有的环境变量。您可以使用clear_env
配置指令禁用它。清理环境后,您可以使用以下env[name] = value
语法添加自己的变量:
; somewhere in the pool configuration file (www.conf by default)
; declare a new environment variable
env[APP_ENV] = production
; reference an existing environment variable
env[DB_NAME] = $DB_NAME
第二个选项是从Web服务器传递环境变量。你可以在Caddy中使用env
参数,在NGINX中使用fastcgi_param
,在apache中使用PassEnv
或配置SetEnv
。
确认无法从互联网访问PHP-FPM!否则,任何人都可以使用Web服务器用于将环境变量传递给应用程序的相同机制来注入环境变量。检查listen.allowed_clients设置。
.ENV
因为设置环境变量很麻烦,所以Ruby社区提出了.env
文件约定。您将环境变量声明在.env
项目根目录中调用的文件中,并且库在引导时将所有环境变量加载到应用程序中。
最初.env文件不适合生产。将所有秘密保留为纯文本并且解析文件很慢是不安全的。但是,它似乎相当普遍。
Laravel只通过访问配置文件中的环境变量然后缓存配置来解决解析开销问题。Symfony 建议不要在生产中使用他们的DotEnv组件。
云提供商
云提供商通常会这么做,所以请先检查一下。Heroku有一个config:set
命令。Laravel Forge允许从控制面板添加环境变量。Fortrabbit也是如此。
如果您正在配置自己的服务器,则上面列出的选项可以使用,但它们不是很安全。Kubernetes支持定义环境变量并为环境变量使用秘密。如果您正在使用Hashicorp的Vault和Consul,您可以使用envconsul来启动填充了环境变量的流程。
阅读环境变量
在PHP中有3种不同的方法来读取环境变量。如果要在PHP中设置环境变量(.env库的工作方式),还有3种方法可以设置它们。
了解差异非常重要,因为每种方法都可以根据服务器的配置方式返回不同的数据。
$ _SERVER和$ _ENV
该$_SERVER
超全局包含除任何Web服务器一起传递环境变量。
如果从variables_order
指令中删除'S' $_SERVER
将不会被填充。
» APP_ENV=local php -d variables_order=EGPC -r 'var_dump($_SERVER["APP_ENV"] ?? false);'
bool(false)
还有一个$_ENV
超全球。就像$_SERVER
可以通过E
从variables_order
指令中删除它来禁用它。开发和生产的默认值是GPCS
,意味着$_ENV
您的服务器很可能是空的。
那么$_ENV
和之间的区别是$_SERVER
什么?在CGI模式下,什么都没有。使用内置Web服务器时,仅$_ENV
包含环境变量,并且仅$_SERVER
包含服务器变量,例如标头,路径和脚本位置。当运行CLI脚本$_SERVER
并且$_ENV
包含环境变量时,$_SERVER
还包含请求信息和CLI参数。
最终,由SAPI来填充每个超全球。
$_ENV
并且$_SERVER
是两个不同的变量 - 改变一个不会改变另一个。 $_ENV
并$_SERVER
在第一次访问时填充。如果auto_globals_jit
禁用该指令,则会在脚本启动时填充它们。如果在填充变量(即通过调用putenv
)后更改环境,则不会更新超全局。同样更新$_ENV
或$_SERVER
不会改变实际环境。如果您想改变您必须呼叫的实际环境putenv
。
GETENV
该getenv
功能与$_ENV
超全球功能类似。但是,与superglobals不同,getenv
不能用variables_order
指令禁用。
» APP_ENV=local php -d variables_order= -r 'var_dump(getenv("APP_ENV"));'
string(5) "local"
那么当你打电话时会发生什么getenv('APP_ENV')
?让我们看看源代码,了解它是如何工作的。
PHP_FUNCTION(getenv)
{
// ...
if (!local_only) {
ptr = sapi_getenv(str, str_len);
if (ptr) {
RETVAL_STRING(ptr);
efree(ptr);
return;
}
}
// ...
}
首先,我们调用sapi_getenv
如果local_only
参数是假的。此函数是SAPI用于加载常规环境中不存在的变量的挂钩。这是getenv
可以返回HTTP标头的原因。
PHP_FUNCTION(getenv)
{
// ...
/* system method returns a const */
ptr = getenv(str);
if (ptr) {
RETURN_STRING(ptr);
}
RETURN_FALSE;
}
接下来我们调用getenv
c函数(在Unix上; Windows调用GetEnvironmentVariableW
)。这非常重要。超级全局只会在首次初始化时读取系统环境变量。 getenv
每次调用时都会读取系统环境变量。如果您使用线程,这将成为一个问题。
线程安全
c函数getenv
不需要是线程安全的。如果getenv
在另一个线程正在调用时调用putenv
它,则可能导致分段错误。
使用以下代码可以很容易地说明这一点。您将需要编译PHP zts
并pthreads
启用扩展程序来运行它。
<?php
$worker = new class() extends \Thread {
function run()
{
while (true) {
putenv('RAND' . rand() . '=value');
}
}
};
$worker->start();
while (true) {
getenv('FOO');
}
如果从命令行运行此命令,则应在30秒内看到段错误。
» php env_crash.php
Segmentation fault
这篇优秀的文章深入解释了这个问题,并包含了一个示例c程序,如果您没有pthreads
安装,可以运行该程序。
我们如何避免分段错误?一些开发人员已经开始建议您使用$_SERVER
或$_ENV
代替getenv
读取环境变量。这当然可以避免这个问题,但并不像你想象的那么容易。
如上所述,如果您不控制服务器,则无法保证$_SERVER
并将$_ENV
启用。 $_ENV
默认情况下禁用。它不太可能$_SERVER
被禁用,但如果您使用$_SERVER
您的应用程序将无法使用PHP的内置Web服务器。
其次,很难保证你所依赖的所有C库都会避免getenv
。例如, 如果您未指定,则finfo_open
调用。偶数并在初始化时调用。您需要审核所有libc,PHP,每个PHP库,并保证不会调用每个扩展。getenv
$magic_file
$_SERVER
$_ENV
getenv
getenv
第三,如果你正在使用pthreads,那么superglobals在工作线程中是空的。访问主线程的环境变量的唯一方法是调用getenv
。
一个更简单的解决方案是避免调用putenv
工作线程。如果您使用putenv
填充环境变量,则只需执行一次。每个工作线程都将继承父线程的环境变量,因此您无需再次填充它们。在创建线程之前获取引导程序,您将不会遇到任何问题。
一些开发人员使用线程Web服务器,因此他们无法在工作线程之外实际执行其代码。PHP 无论如何都不应该与线程服务器一起使用,但是如果你坚持这样做,你可以通过只调用putenv
if getenv
返回false并将整个事物包装在互斥锁中来避免分段错误。由于环境变量在线程之间共享,因此只有第一个请求才会调用putenv
。
产卵过程
环境变量不仅在线程之间共享,它们也与子进程共享。当你生成一个进程有exec
,passthru
,system
,shell_exec
,proc_open
,或反引号操作符子进程继承父进程的环境。
» APP_ENV=local php -r 'passthru("env");'
TERM_PROGRAM=Apple_Terminal
SHELL=/bin/zsh
TERM=xterm-256color
TMPDIR=/var/folders/9_/wn_qf7x97tl1l86lfxl1shg00000gn/T/
USER=matt
APP_ENV=local
如果您将环境变量用于机密并生成不受信任的子进程,则这可能是一个安全问题。
如上所述,添加变量$_ENV
或$_SERVER
不将其添加到实际环境中。只返回的环境变量getenv
将传递给子进程。添加的任何变量putenv
都将传递给子进程,因为它putenv
会修改环境。
proc_open
允许您指定应传递给子流程的环境变量。您可以proc_open
在不希望将应用程序机密传递给子进程的情况下使用。
该Symfony的进程组件 并 通过$_SERVER
,并$_ENV
在默认情况下的子进程。为了防止您可以显式设置环境变量。
注意HTTP标头
我在上面提到了这一点,但重要的是它应该属于自己的部分。 访问环境变量的每种方法都可以返回HTTP标头,包括getenv
。
当头文件包含在CGI应用程序中的环境中时,它的前缀是HTTP_
。由于宣布了“httpoxy”漏洞,PHP不会让Proxy
标题覆盖HTTP_PROXY
,但是以HTTP_
(ie HTTP_PROXIES
)开头的任何其他环境变量仍然会受到影响。总之,永远不要使用以...开头的环境变量HTTP_
。
getenv
允许你传递第二个参数,local_only
。如果为true ,则不会检查SAPI。if是否local_only
为真HTTP标头,fpm.conf中设置的变量以及Web服务器配置中设置的变量将被排除。local_only
返回所有环境变量时无法使用- getenv(null, true)
将返回false。
保密秘密
泄漏环境变量要比泄漏PHP变量容易得多。如果您将环境变量用于保密,那么了解泄漏环境变量的所有方法非常重要。
来自$_ENV
,$_SERVER
和的环境变量getenv
在phpinfo
输出中可见。
在命令行上传递的环境变量可以显示在shell历史记录中。
您可以查看正在运行的进程的环境变量,但前提是该进程是您的进程或您是root用户。
如果您使用的是.env
文件并且它位于公共目录中,则Web服务器将以明文形式提供该文件。注意路径遍历攻击。如果你被欺骗要求或包含.env
文件,秘密将在PHP脚本的输出中呈现。
环境变量是全球性的。任何PHP或C代码都可以访问它们,而无需了解您的应用程序。
环境变量传递给子进程,线程和分支。
记录错误处理程序很常见$_SERVER
。两个哨兵和减速板这样做。
getenv并不总是有效
在研究这篇文章时,我遇到了一些getenv
我以前从未见过的错误。
当使用`getenv`接收`fastcgi_param`时,其他任何人都会遇到这种非常奇怪的#php行为?我可以使用`getenv`访问它,但不能使用`getenv($ key)`。pic.twitter.com/98ah3yJl1n
- 马特艾伦(@__mattallan),2019年3月22日
另一个奇怪的`getenv`事情:如果你启用了auto_globals_jit,你必须访问脚本中的某个地方(甚至在!之后)才能运行getenv。 pic.twitter.com/Fvhkn3Zk8s
- 马特艾伦(@__mattallan),2019年3月22日
第一个问题只发生在Web服务器设置的变量上,即NGINX fastcgi_param
。第二个问题发生在由Web服务器设置或由PHP-FPM设置的变量中。
第二个问题非常惊人。当你考虑如何auto_globals_jit
工作(“在编译期间检查SERVER,REQUEST和ENV变量的使用”)时,这是有道理的,但我认为这不是故意的。
结论
PHP中的环境变量令人困惑,不一致,有时甚至是危险的。CGI通过将用户输入合并到环境变量中使问题变得更糟。如果你不得不使用它们getenv
,请避免调用putenv
线程,永远不要信任一个以变量开头的变量HTTP_
,并验证你没有泄露其他进程或服务的秘密。如果您正在编写将由没有经验的系统管理员配置的软件,那么最好完全避免使用环境变量。
免责声明:我不是安全专家,本文未涵盖所有可能的安全风险。由您决定什么对您的情况安全。
来自:https://mattallan.me/posts/how-php-environment-variables-actually-work/
转载请注明:来自Lenix的博客 ,地址 http://blog.p2hp.com/archives/6124