1. 程式人生 > 實用技巧 >PHP8.0新特性

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可以吃的飯應該還有很多。