Skip to content
isdnetworks
Go back

한 겹 대신 여러 겹

(수정됨)

관리자만 쓸 수 있는 기능을 Blade 에서 @if($isAdmin) 으로 버튼을 숨기는 것으로 막고 있었다. 버튼은 안 보이지만 DELETE /admin/products/{id} 를 직접 부르면 그대로 실행됐다.

Table of contents

Open Table of contents

원인 — 화면만 막고 서버는 안 막은 상태

화면에서 안 보이는 것과 실행할 수 없는 것은 다른 사실인데 그 구분이 코드에 반영돼 있지 않았다. 화면은 사용성을 위한 것이고 접근 통제는 서버에서 이뤄져야 한다.

한 겹으로 막으면 그 한 겹이 뚫릴 때 전부 뚫린다. 그래서 어느 층에서 무엇을 막을지를 먼저 정했다.

조치 — 층마다 막되 정하는 범위

화면과 라우팅과 컨트롤러와 서비스와 데이터베이스 다섯 층이 후보였고 전부에 넣지는 않았다. 두 가지 방법이 있었는데 다섯 층 전부에 넣는 것과 화면과 라우팅과 서비스 셋에만 넣는 것이다. 뒤쪽을 골랐다. 이유는 컨트롤러는 라우팅의 can: 미들웨어와 중복이고 데이터베이스 계정 분리는 운영 부담이 커서다. 대신 데이터베이스 층에서는 아무것도 막지 못하는 것을 감수했다.

Laravel 공식 문서대로 라우트에는 can: 미들웨어를 걸고 서비스에서는 Gate::allows 를 다시 부른다. 겹겹으로 막는다는 말이 모든 층에 넣는다는 뜻은 아니었다.

배경 — 중복이 곧 겹겹인 이유

라우팅과 서비스에서 같은 확인을 두 번 하는 것이 중복으로 보이지만 그 중복이 방어의 실체였다. routes/web.php 에서 미들웨어를 실수로 빠뜨려도 서비스가 막고 서비스를 artisan 명령에서 불러도 막힌다.

한 곳에서만 확인하면 그 한 곳을 안 거치는 경로가 생겼을 때 방어가 통째로 사라진다. 두 번 확인하는 비용이 작으므로 그 중복을 감수하는 쪽이 맞았다.

검증 — 층마다 다른 것을 보게 하기

전부 같은 확인을 하면 같은 실수를 층마다 반복하게 된다. 그래서 서비스 층에서는 ProductPolicy 로 권한만이 아니라 seller_id 대상 범위까지 보게 했다.

관리자여도 다른 조직의 대상은 못 건드리게 하니 한 층이 뚫려도 다른 층이 잡는 구조가 됐다. 각 층을 하나씩 건너뛰는 PHPUnit 시험도 만들어서 라우팅을 안 거치고 서비스를 직접 불렀을 때 AuthorizationException 이 나는지 확인했다.

주의 — 두께와 조회 횟수

되돌리기 어려운 동작일수록 두껍게 막아서 조회는 권한 확인만 하고 전체 삭제는 권한 확인과 기록과 확인 절차와 다른 관리자의 승인까지 요구했다. 막는 것과 별개로 권한 없는 시도를 audit_logs 에 남기니 며칠 만에 몇 건이 나왔고 그중 하나는 확인이 필요한 것이었다.

층을 늘리니 한 요청에 같은 roles 조회가 여러 번 나가는 문제가 생겼다. 한 요청 안에서는 값이 안 바뀌므로 once() 로 요청 단위로 담아 두게 해서 조회는 한 번으로 줄였는데 막는 층을 늘리는 것과 조회를 늘리는 것은 별개의 문제였다.

정리


Share this post on:

Previous Post
복제가 데려가야 하는 것들
Next Post
고치다 떨어뜨린 검증분