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

參考鏈接:


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1412/