어떤 웹 어플리케이션의 관리자 시스템이 있다고 가정하자.
관리자 시스템이기 때문에 관리자의 권한을 엄격하게 통제하는 것이 중요할 것이다.
그런데 대부분의 시스템에서는 화면단위로 권한을 통제하는 경우가 많다. 그런데 화면단위로 권한을 통제하는 것은 권한이 낮은 사용자가 직접 URL을 조합하여 서비스를 호출할 수 있는 문제가 있다.
그래서 화면보다 더 세부적인 단위로 권한을 통제하는 것이 보안상으로 더 안전하지만, 수 많은 서비스에 직접 권한을 일일히 지정하는 것은 상당히 번거롭고 관리하기도 힘들다.
그래서 비교적 작은 수고를 들이면서 서비스단위의 권한을 통제하는 방법에 대해 생각해 보았다.
1
2
3
4
5
6
7
private final Map<String,Object> grantedAuths = new HashMap<>();
public UserMngController() {
grantedAuths.put("/view/LoanMain.jsp" , "COLR001"); // 대출반납
grantedAuths.put("/view/UserMngMain.jsp" , "COLR009"); // 이용자관리
grantedAuths.put("/view/UserPrivacyMngMain.jsp" , "COUI"); // 개인정보관리
}
위와 같은 형태로 화면의 URL과 화면ID가 1:1로 매핑된다고 가정한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SuppressWarnings("unchecked")
private boolean validateAuth(HttpServletRequest request) {
HttpSession session = request.getSession(false);
String referer = request.getHeader("referer");
if(session == null || referer == null || referer.isEmpty())
return false;
String matchedPage = grantedAuth.keySet().stream()
.filter(referer::endsWith)
.findFirst()
.orElse(null);
if(matchedPage == null) // 지정된 페이지가 아닌 다른 페이지에서 서비스를 호출했으면 서비스 거절.
return false;
String pageId = (String) grantedAuth.get(matchedPage);
if (pageId == null || pageId.isEmpty()) // URL과 pageId가 매칭이 안되면 서비스 거절
return false;
List<String> operatorAuthCodes = (List<String>)session.getAttribute("operatorAuthCodes");
return operatorAuthCodes != null && operatorAuthCodes.contains(pageId);
}
모든 서비스를 호출할 때마다 관리자의 권한을 확인하는 SQL를 실행시키는 것은 너무 부하가 클 것이기 때문에 로그인 시점에서 세션 또는 토큰에다가 권한을 저장한다. 그리고 서비스가 호출될 때마다 validateAuth 메소드를 호출하거나 AOP를 사용하여 권한을 확인한다.
validateAuth에서는 referer를 기반으로 어떤 페이지에서 서비스를 호출했는지 확인한다.
referer는 위변조 가능한 헤더이기 때문에 100% 신뢰할 수는 없지만, 현재 세션 또는 토큰 기반으로 재검증 하기 때문에 어느정도의 방어 효과를 기대할 수 있다.
위의 코드를 적용하면 이용자 관리페이지에 접근권한이 있는 관리자가 이용자 관리페이지에서 서비스를 호출했을 때에만 정상 응답하고, 임의로 URL를 조합하거나 이용자 관리페이지가 아닌 다른 페이지에서 서비스를 호출했을 때에는 서비스가 거절된다.
현재 상황으로는 위의 코드는 기대되로 잘 동작하지만, 추후에 취약점이 발견되면 개선할 필요가 있다.