Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.springframework.core.annotation.Order
import org.springframework.session.web.http.SessionRepositoryFilter
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.UrlPathHelper

// prod·전 환경의 /admin 게이트 — 슬랙으로 검증된 세션 + allowlist IP + 세션-IP 바인딩 셋 다 맞아야 통과, 아니면 404.
// password 가 아니라 "슬랙 링크 클릭으로 발급된 세션"이 신원이다. 미허용은 401/302 가 아니라 404(존재 숨김).
Expand Down Expand Up @@ -62,15 +63,26 @@ class AdminAccessFilter(
}
}

// /admin 과 /admin/** 만 게이트. /admin-assets·/admin-access 는 다른 prefix 라 제외(공개 진입·정적).
override fun shouldNotFilter(request: HttpServletRequest): Boolean {
val uri = request.requestURI
return !(uri == "/admin" || uri.startsWith("/admin/"))
}
override fun shouldNotFilter(request: HttpServletRequest): Boolean = !isGatedRequest(request)

// setStatus 로 막는다(sendError 금지) — sendError 는 /error 로 ERROR 디스패치를 일으켜 메인 체인이 401 로
// 가로채면 "존재 숨김(404)" 의도가 깨진다. 체인을 더 진행하지 않으니 빈 404 로 응답이 닫힌다.
private fun deny(response: HttpServletResponse) {
response.status = HttpServletResponse.SC_NOT_FOUND
}

companion object {
// 게이트 판정 경로를 Spring 라우팅과 동일하게 정규화한 뒤 매칭한다 — raw requestURI 로 판정하면
// Spring 은 정규화 경로로 라우팅하는데 판정은 원문이라 불일치가 생겨, `/%61dmin/announcements`(percent-encoding)·
// `/admin;x=1`(matrix param) 처럼 필터는 안 걸고 dispatcher 는 서빙하는 우회가 뚫린다. 그 경로로 들어오면
// 세션·세션-IP 바인딩·allowlist 검사가 통째로 생략된 채 백오피스에 닿는다(#986).
// EnvironmentAccessFilter 가 같은 이유로 쓰는 UrlPathHelper(removeSemicolonContent·urlDecode 기본 on)를 그대로 쓴다.
private val PATH_HELPER = UrlPathHelper.defaultInstance

fun isGatedRequest(request: HttpServletRequest): Boolean = isGatedPath(PATH_HELPER.getPathWithinApplication(request))

// /admin 과 /admin/** 만 게이트. /admin-assets·/admin-access 는 다른 prefix 라 제외(공개 진입·정적).
// 세그먼트 경계로 매칭해 /admin-access 류 과매칭을 막는다.
fun isGatedPath(uri: String): Boolean = uri == "/admin" || uri.startsWith("/admin/")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.depromeet.piki.admin.access
import org.junit.jupiter.api.Test
import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties
import org.springframework.core.annotation.OrderUtils
import org.springframework.mock.web.MockHttpServletRequest
import org.springframework.session.web.http.SessionRepositoryFilter
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

Expand Down Expand Up @@ -40,4 +42,40 @@ class AdminAccessFilterTest {
"미허용 요청이 404 가 아니라 메인 체인의 401 로 새어 존재 숨김이 깨진다.",
)
}

@Test
fun `admin 과 admin 하위 경로만 게이트한다`() {
assertTrue(AdminAccessFilter.isGatedPath("/admin"))
assertTrue(AdminAccessFilter.isGatedPath("/admin/announcements"))
assertTrue(AdminAccessFilter.isGatedPath("/admin/item-quota"))
}

@Test
fun `공개 진입과 정적 prefix 는 게이트하지 않는다`() {
// /admin-access 는 grant 진입점이라 세션 이전에 닿아야 하고, /admin-assets 는 정적 prefix 다.
// startsWith("/admin") 로 판정하면 둘 다 과매칭돼 grant 흐름 자체가 막힌다.
assertFalse(AdminAccessFilter.isGatedPath("/admin-access/grant"))
assertFalse(AdminAccessFilter.isGatedPath("/admin-assets/app.css"))
assertFalse(AdminAccessFilter.isGatedPath("/api/v1/wishlists"))
}

@Test
fun `percent-encoding·matrix parameter 로 위장한 admin 경로도 정규화 후 게이트한다`() {
// raw requestURI 로 판정하면 필터는 안 걸고 dispatcher 는 정규화 경로로 서빙해, 세션·IP 바인딩·allowlist
// 검사를 통째로 건너뛴 채 백오피스에 닿는 우회가 뚫린다(#986). dispatcher 와 같은 경로를 봐야 한다.
assertTrue(AdminAccessFilter.isGatedRequest(req("/%61dmin/announcements"))) // %61 = 'a'
assertTrue(AdminAccessFilter.isGatedRequest(req("/admin/%69tem-quota"))) // %69 = 'i'
assertTrue(AdminAccessFilter.isGatedRequest(req("/admin;x=1")))
assertTrue(AdminAccessFilter.isGatedRequest(req("/admin;x=1/announcements")))
}

@Test
fun `정규화 후에도 공개 진입 prefix 는 통과한다`() {
// 위 우회 차단이 grant 진입까지 함께 막아버리면 백오피스에 아무도 못 들어간다.
assertTrue(AdminAccessFilter.isGatedRequest(req("/admin")))
assertFalse(AdminAccessFilter.isGatedRequest(req("/admin-access/grant")))
assertFalse(AdminAccessFilter.isGatedRequest(req("/%61dmin-access/grant")))
}

private fun req(uri: String) = MockHttpServletRequest().apply { requestURI = uri }
}
Loading