PHP8.0新特性
經歷了近半年的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可以吃的飯應該還有很多。