iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

一套真實運作中的 Laravel 系統,拆解它的原生機制系列 第 9

Day 09:角色可見性控制——為什麼最高權限角色不能靠一般權限判斷保護

  • 分享至 

  • xImage
  •  

前言:如果一個角色能編輯所有角色,包括「能編輯所有角色」這個角色本身呢?

「權限管理不就是判斷『這個使用者有沒有某個權限字串』嗎?」

昨天看到的 BannerPolicy 確實是這樣運作的——每個方法委派給底層權限套件,查一個字串權限存不存在。但這個模式有一個邊界情況沒處理到:如果系統裡有一個「角色管理」的權限,理論上任何被授予這個權限的人,都能編輯任何角色——包括最高權限的那個角色本身。 今天要看這個系統怎麼處理這個邊界情況。

今日目標

  • 理解「一般權限判斷」在保護「權限管理本身」時會出現的漏洞
  • 看一個真實案例:RolePolicy 怎麼對「最高權限角色」做特殊判斷
  • 認識 Gate 之外,Policy 也可以定義框架不認得的自訂方法
  • 建立一個判斷基準:什麼情境需要跳脫一般權限字串,加一層寫死的檢查

本文主體

一般權限判斷保護不了什麼

昨天看到的權限模式是:一個角色被指派一組權限字串,Policy 方法檢查使用者有沒有對應字串。這個模式對大多數資料類型都夠用——輪播圖、新聞、頁面,權限跟資料是分開的兩件事。

但「角色」這個資料本身有一個特殊之處:如果有一個「角色管理」的權限字串,任何被指派這個字串的角色,都能編輯所有角色——包括最高權限角色。也就是說,只要有人不小心(或蓄意)把「角色管理」權限指派給一個非最高權限的角色,那個角色就能把自己的權限一路提升到跟最高權限角色一樣,形成一個權限提升的漏洞。單純的字串權限判斷,防不了這種情況。

真實案例:RolePolicy 對特定角色的額外檢查

這個系統的 RolePolicy 對「最高權限角色」(角色 ID 為 1)做了特殊處理:

class RolePolicy
{
    public function view(User $user, Role $role): ?bool
    {
        if ($role->id !== 1) {
            return $user->can('View:Role');
        }

        return $user->hasRole('Super Admin');
    }

    public function update(User $user, Role $role): ?bool
    {
        if ($role->id !== 1) {
            return $user->can('Update:Role');
        }

        return $user->hasRole('Super Admin');
    }

    public function delete(User $user, Role $role): ?bool
    {
        if ($role->id !== 1) {
            return $user->can('Delete:Role');
        }

        return false;
    }
}

每個方法先判斷「這個角色是不是最高權限角色本身」——如果不是,走一般的權限字串判斷;如果是,就完全不理會一般的權限字串,改用 $user->hasRole('Super Admin') 這種直接檢查角色本身的方式。注意 delete 方法對最高權限角色永遠回傳 false——這個角色連被授予「角色管理」權限的最高權限使用者,都不能刪除,這是一個完全寫死、沒有任何權限字串能繞過的規則。

為什麼要這樣設計

這個設計的核心邏輯是:「能不能管理角色」跟「能不能碰到最高權限角色」是兩件不同層級的事,不能用同一組判斷邏輯處理。 一般角色的權限可以透過資料庫設定靈活調整(誰能看、誰能改、誰能刪),但「最高權限角色」牽涉到整個系統的權限邊界,這個邊界不應該被資料庫裡的權限設定意外打開——就算有人不小心把「角色管理」權限指派給一個不該有這麼高權限的角色,那個角色也不會因此能碰到最高權限角色本身。

❌ 用同一套權限字串判斷保護所有角色,包括最高權限角色

public function update(User $user, Role $role): ?bool
{
    return $user->can('Update:Role');
}

只要有人被指派了 Update:Role 這個權限字串,不管針對哪個角色都能更新——包括最高權限角色。權限提升的路徑完全暢通。

✅ 對最高權限角色額外硬編碼一層檢查

public function update(User $user, Role $role): ?bool
{
    if ($role->id !== 1) {
        return $user->can('Update:Role');
    }

    return $user->hasRole('Super Admin');
}

一般角色走靈活的權限字串判斷,最高權限角色本身則跳過權限字串、直接檢查身份。

一個 Policy 也能有框架不認得的自訂方法

RolePolicy 還定義了一個框架不會自動呼叫的方法:

public function assignSuperAdmin(User $user): bool
{
    return $user->isSuperAdmin();
}

viewAnyviewcreateupdatedelete 這幾個名稱,Laravel 框架認得,會在對應情境自動呼叫;但 Policy 類別本身只是一個普通的 PHP 類別,可以自訂任何方法名稱,只要呼叫端明確用 $user->can('assignSuperAdmin') 呼叫就行。這個方法用在「後台編輯角色時,能不能把最高權限指派給某個角色」這個更細緻的判斷情境,不屬於標準 CRUD 動作,所以用一個自訂方法名稱表達。

今日思考題

你的系統裡有沒有一個「權限管理權限本身」的邊界情況——一個被授予管理權限的角色,能不能把自己的權限一路提升到最高?如果可以,這是刻意設計的,還是被忽略的漏洞?

今日重點回顧

  • 一般的權限字串判斷,防不了「角色管理權限」被用來提升自己權限到最高等級的情況
  • 真實案例:RolePolicy 對「最高權限角色」(id=1)額外做硬編碼檢查,跳過一般權限字串判斷
  • delete 方法對最高權限角色永遠回傳 false,這是完全寫死、任何權限字串都無法繞過的規則
  • Policy 類別可以自訂框架不認得的方法名稱,處理不屬於標準 CRUD 的細緻授權情境

明日預告

明天要看一支很單純但很真實的 Command——怎麼用一支一次性的指令,修正資料遷移後留下的欄位缺失。


上一篇
Day 08:Policy 授權基礎——一份 Policy 檔案怎麼決定誰能看見什麼
下一篇
Day 10:一支 Command,修一次資料遷移後留下的欄位缺失
系列文
一套真實運作中的 Laravel 系統,拆解它的原生機制14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言