作者:phith0n
原文鏈接:https://mp.weixin.qq.com/s/0HSAPYY2PjbwEN3MhI4SkA
經歷了近半年的alpha版本測試后,PHP在2020年11月26號正式發布了8.0版本:https://www.php.net/releases/8.0/en.php
今天我們就來瀏覽一下PHP 8.0中出現的主要特性,以及它給我們安全研究人員帶來的挑戰。
一、命名參數 Named Arguments
PHP 8 以前,如果我們需要給一個函數的第N個參數傳參,那么這個參數前面的所有參數,我們都需要傳參。但是實際上有些參數是具有默認值的,這樣做顯得多此一舉。
比如,我們要給htmlspecialchars的第4個參數傳遞false,在PHP 8 以前需要傳入4個參數:
htmlspecialchars($string, ENT_COMPAT | ENT_HTML401, 'UTF-8', false);
在8.0以后增加了命名參數,我們只需要傳遞必需的參數和命名參數即可,方便了很多:
htmlspecialchars($string, double_encode: false);
二、屬性注釋 Attributes
屬性注釋是我自己取得名字,在英文原文中是單詞「Attributes」(在C++、C#、Rust里也是相同的單詞,但翻譯有些差別)。這個新語法有點類似Python里的修飾器,以及Java里的Annotation。
但是,PHP里Attributes的作用還是更偏向于替換以前的doc-block,用于給一個類或函數增加元信息,而不是類似Python的修飾器那樣,可以動態地劫持函數的輸入與輸出。
屬性注釋的簡單例子:
#[ListensTo('error')]
function onerror() {
// do something
}
上面這個例子實際測試你會發現,屬性注釋里的東西也真的只是一個注釋,執行上述的代碼也不會去調用ListensTo類。這也印證了上面所說的,Attributes只是對以前doc-block的一個接納,而非創造了一種HOOK函數的方式。
如果你需要執行Attributes里面的代碼,仍然需要通過反射來做到,比如:
#[Attribute]
class ListensTo {
public string $event;
function __construct($event)
{
$this->event = $event;
}
}
#[ListensTo('error')]
function onerror()
{
// do something
}
$listeners = [];
$f = new ReflectionFunction('onerror');
foreach($f->getAttributes() as $attribute) {
$listener = $attribute->newInstance();
$listeners[$listener->event] = $f;
}
我模擬了一個設計模式中監聽模式的事件處理方法注冊過程,相比于以前解析Doc-Block的過程,這個流程要更加簡單。
相比于其他的新特性,框架或IDE的設計者可能會研究的更深,普通開發者只需要按照框架的文檔簡單使用這個語法即可。
三、構造器屬性提升 Constructor property promotion
這是一個利國利民的好特性,可以延長鍵盤的壽命……PHP 8以前,我們定義一個類時,可能會從構造函數里接收大量參數并賦值給類屬性,如:
class Point {
public float $x;
public float $y;
public float $z;
public function __construct(
float $x = 0.0,
float $y = 0.0,
float $z = 0.0,
) {
$this->x = $x;
$this->y = $y;
$this->z = $z;
}
}
實際上這已經形成了一種范式,我們要不厭其煩地進行定義->傳遞->賦值的過程。PHP 8以后給出了一種更加簡單的語法:
class Point {
public function __construct(
public float $x = 0.0,
public float $y = 0.0,
public float $z = 0.0,
) {}
}
直接在構造函數的參數列表位置完成了類屬性的定義與賦值的過程,減少了大概三分之二的代碼量。
另外提一句,這個RFC的作者是Nikita Popov,也就是著名的開源項目PHP-Parser的作者,做PHP代碼分析的同學應該經常和這個項目打交道。他今年去了PHPStorm團隊,相信這個老牌IDE在Nikita的加持下會變得更加好用。
四、聯合類型 Union types
PHP 8 以前的Type Hinting,只支持使用一個具體的Type,比如:
function sample(array $data) {
var_dump($data);
}
這個功能雞肋的一點是,有些地方接受參數類型可能有多個類型,或者支持傳入null。
在7.1時解決了null的問題:
function sample(?array $data) {
var_dump($data);
}
但是仍然無法指定多個類型hint。
PHP 8 中總算支持了Union types,我們可以通過|來指定多個類型Hint了:
function sample(array|string|null $data) {
var_dump($data);
}
五、Match 語法
這是一個新的關鍵字match,這也是一個利國利民的好特性,又一次延長了鍵盤的壽命……
在PHP 8.0以前,我們要根據一個名字來獲取一個值,通常需要借助switch或者數組,比如:
switch ($extension) {
case 'gif':
$content_type = "image/gif";
break;
case 'jpg':
$content_type = "image/jpeg";
break;
case 'png':
$content_type = "image/png";
break;
}
echo $content_type;
現在可以簡化成一個「表達式」:
echo match ($extension) {
'gif' => "image/gif",
'jpg' => "image/jpeg",
'png' => "image/png"
};
六、Null安全的操作符 Nullsafe operator
這又又又是一個利國利民的好特性,又又又一次延長了鍵盤的壽命……
在PHP 8以前,如果封裝的較多,我們經常出現一種情況:一個函數接受X對象,但又可能是null,此時我在使用X對象屬性前,就需要對null進行判斷,以免出現錯誤。
在對象較多時,容易出現多層嵌套判斷的情況,比如:
$country = null;
if ($session !== null) {
$user = $session->user;
if ($user !== null) {
$address = $user->getAddress();
if ($address !== null) {
$country = $address->country;
}
}
}
PHP 8 以后增加了一個新語法:?->,非常類似于PHP7里引入的??。就是在取屬性前,PHP會對對象進行判斷,如果對象是null,那么就直接返回null了,不再取其屬性:
$country = $session?->user?->getAddress()?->country;
七、字符串數字弱類型比較優化
這一個改動可能會對安全漏洞挖掘的影響較大。PHP 8 以前,在使用==比較或任何有弱類型轉換的情況時,字符串都會先轉換成數字,再和數字進行比較。
比如,這個代碼在PHP 8以前的結果是true和0,在PHP 8以后得到的則是false和1:
var_dump('a' == 0);
switch ('a') {
case 0:
echo 0;
break;
default:
echo 1;
break;
}
老的弱類型可能會有什么安全問題呢?我曾經挖掘到的一個真實案例,大概代碼是這樣:
$type = $_REQUEST['type'];
switch ($type) {
case 1:
$sql = "SELECT * FROM `type_one` WHERE `type` = {$type}";
break;
case 2:
$sql = "SELECT * FROM `type_two` WHERE `type` = {$type}";
break;
default:
$sql = "SELECT * FROM `type_default`";
break;
}
開發者認為$type是1和2的時候才會進入SQL語句拼接中,但實際我們傳入1 and 1=2即可進入case 1,導致SQL注入漏洞。
PHP 8以后徹底杜絕了這種漏洞的產生。
八、內部函數嚴格參數檢查
在PHP 8 以前,如果我們使用內部函數時傳入的參數有誤(比如,參數類型錯誤,參數取值錯誤等),有時會拋出一個異常,有時是一個錯誤,有時只是一個警告。在PHP 8 以后,所有這類錯誤都將是一個異常,并且導致解釋器停止運行,比如:
strlen([]); // TypeError: strlen(): Argument #1 ($str) must be of type string, array given
array_chunk([], -1); // ValueError: array_chunk(): Argument #2 ($length) must be greater than 0
這個改動可能會影響一些安全漏洞的利用,有一些我們之前通過弱類型等tricks構造的POC,在老版本PHP中只是一個警告,不會影響解釋器的執行,但8.0之后將會導致錯誤,也就中斷了執行。
九、JIT
JIT(Just-In-Time)被鳥哥稱為PHP 8 中最重要的改動,我來簡單介紹一下PHP 8 的JIT。
PHP 8 的JIT附加在opcache這個擴展中,opcache本身就是對PHP解釋器的優化。沒有使用opcache時,PHP解釋器是在運行PHP腳本的時候進行“編譯->Zend虛擬機執行”的過程。而opcache的出現實際上就是節省了編譯的時間,代碼在第一次運行時會編譯成opcache能識別的緩存(opcode),之后運行時就免除了編譯的過程,直接執行這段opcode。
而JIT的出現再次優化了這個過程,JIT會將一些opcode直接翻譯成機器碼。這樣PHP解釋器在執行時,如果發現緩存中保存的是機器碼,就會直接交給CPU來執行,又減少了Zend虛擬機執行opcode的時間。
普通開發者可能對JIT比較無感,畢竟大家的性能瓶頸多半出現在IO等問題中,但對于性能要求極高的人或企業來說,JIT的確是對PHP的重要改進。
十、其他可能和安全相關的改動
作為安全研究者,我會更關注的是和安全相關的改動。除了前面提到了弱類型方面的改動外,PHP 8還進行了如下一些和安全相關的改動:
assert()不再支持執行代碼,少了一個執行任意代碼的函數,這個影響還是挺大的。create_function()函數被徹底移除了,我們又少了一個可以執行任意代碼的函數。- libxml依賴最低2.9.0起,也就是說,XXE漏洞徹底消失在PHP里了。
- 繼
preg_replace()中的e模式被移除后,mb_ereg_replace()中的e模式也被徹底移除,再次少了一個執行任意代碼的函數。 - Phar中的元信息不再自動進行反序列化了,
phar://觸發反序列化的姿勢也告別了。 parse_str()必須傳入第二個參數了,少了一種全局變量覆蓋的方法。php://filter中的string.strip_tags被移除了,我在文章《談一談php://filter的妙用》中提到的去除死亡exit的方法之一也就失效了。strpos()等函數中的參數必須要傳入字符串了,以前通過傳入數組進行弱類型利用的方法也失效了。
這些改動,改的我心拔涼拔涼的……我一度認為PHP核心團隊里混入了安全研究者,為什么我們常用的小trick都被改沒了呢?
十一、總結
總結一下PHP 8,我只有兩個感想:
- 我不用擔心鍵盤的壽命了,但是我的頭頂變涼了
- 比頭頂更涼的是我的心,安全真是越來越難做了
好在,現在很多人慢慢轉戰Java,Java可以吃的飯應該還有很多。
參考鏈接:
- https://www.php.net/releases/8.0/en.php
- https://wiki.php.net/rfc/attributes_v2
- https://wiki.php.net/rfc/shorter_attribute_syntax
- https://stitcher.io/blog/attributes-in-php-8
- https://www.laruence.com/2020/06/27/5963.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1412/
暫無評論