LANGUAGE/SPRING 2020. 10. 26. 17:44

XSS (Cross Site Script) 방지하기 - OWASP AntiSamy Project

2017년 웹보안의 가장 큰 위협 10가지 !! (OWASP 발표)

  • A1: Injection (인젝션)
  • A2: Broken Authentication and session management (취약한 인증과 세션 관리)
  • A3: Cross-Site Scripting (크로스 사이트 스크립팅)
  • A4: Broken Access Control (취약한 접근 제어)
  • A5: Security Misconfiguration (보안설정오류)
  • A6: Sensitive Data Exposure (민감 데이터 노출)
  • A7: Insufficient Attack Protection (불충분한 공격 보호)
  • A8: Cross-Site Request Forgery (크로스 사이트 요청 변조) (CSRF)
  • A9: Using Components with Known vulnerabilities (알려진 취약점이 있는 컴포넌트 사용)
  • A10: Unprotected APIs (보호되지 않은 API)

이 Post에서는 OWASP 라이브러리를 사용하여 A3에 해당하는 XSS를 방어하기 위한 방법을 적었다.

1. XSS

1-1. XSS란?

웹페이지에서 사용자 입력값을 악용하여 코드를 심어 악의적인 코드를 수행하도록 하는 공격이다.
쿠키정보 탈취, 악성코드 감염, 웹 페이지 변조 등의 공격을 수행할 수 있다.

1-2. 취약점

HTML을 자유롭게 저장하고 보여주는 기능을 만들었다면, XSS 보안을 고려해야한다.
누군가 악의적인 코드를 심어 해를 가할 수 있기 때문이다.
대체로 HTML/JavaScript를 사용하도록 허용할 경우 발생할 것이다.

  • 예1)

    <script>alert("...");</script>
  • 예2)

    <a href="javascript:alert("...");" >hello?</a>

1-3. 어떻게 방어하지?

보통은 사용자 입력값을 직접 정규식등을 사용해서 검출해내야 할 것이다.
하지만, 우리가 보안전문가가 아닌 이상 모든 경우의 수를 다 알아내고 코딩하기엔 무리가 있다.

1-4. "도와줘요! OWASP !"

OWASP(The Open Web Application Security Project)오픈소스 웹 애플리케이션 보안 프로젝트이다. 주로 웹에 관한 정보노출, 악성 파일 및 스크립트, 보안 취약점 등을 연구한다. 전문가 집단에서 만든 라이브러리를 사용하여 보안취약점을 방어해보자!

2. Dependency

만들어 공개해준 라이브러리를 감사히 활용해보자.

  • Gradle

    • build.gradle

        compile 'org.owasp.antisamy:antisamy:1.5.10'
  • Maven (공식사이트에는 Maven으로 다음과 같이 문서화되어있다.)

    • pom.xml

        <dependency>
            <groupId>org.owasp.antisamy</groupId>
            <projectId>antisamy</projectId>
        </dependency>

3. 정책 선택

  1. 라이브러리(의존성파일)가 잘 연결이 되었다면, 어느 정책파일을 적용할 것인지를 하나 정해야한다.
    • 설명은 https://owasp.org/www-project-antisamy/ 의 Stage 2 - Choosing a base policy file 를 참조한다.
    • 보안 전문가가 아닌이상 다 꼼꼼히 알고 따지고 할 수 없을 것으로 보인다.
  2. 대충! 일단 antisamy-myspace.xml를 적용하기로 했다. 그게 어딨는데?
    • 의존성파일로 연결된 antisamy-1.5.10.jar 파일 안에는 다음 정책파일들이 존재한다는 것을 확인. (IDE의 도움을 받아서 확인)
      • antisamy.xml
      • antisamy-anythinggoes.xml
      • antisamy-ebay.xml
      • antisamy-myspace.xml
      • antisamy-slashdot.xml
      • antisamy-tinymce.xml
    • jar 파일 안에 있는 해당 xml파일을 읽을 수 있는 환경이라면 오케이! 하지만,
    • 그렇지 않다면 파일을 복사하여 원하는 곳으로 옴겨서 사용하자

4. 구현

  • 간단한 예제

      import org.owasp.validator.html.*;
    
      String someHTML = "<script> alert(0); </script> how are you?"
      File policyFile = new File("/path/to/policy/antisamy-something.xml")
    
      Policy policy = Policy.getInstance( policyFile );
      AntiSamy as = new AntiSamy(policy);
      CleanResults cr = as.scan(someHTML);
    
      String xssRemovedHTML = cr.getCleanHTML();
  • 설명

    1. 정책파일(.xml)로부터 Policy 를 객체를 생성한다.

       File policyFile = new File("/path/to/policy/antisamy-something.xml")
       Policy policy = Policy.getInstance( policyFile );
    2. Policy객체로 AntiSamy객체를 생성한다.

       AntiSamy as = new AntiSamy(policy);
    3. String 형식의 html문을 검사(scan)한다. 검사결과인 CleanResults 객체를 받는다.

       String someHTML = "<script> alert(0); </script> how are you?"
       CleanResults cr = as.scan(someHTML);
    4. 검사결과 다음 자료들을 얻을 수 있다.

      • 검출된 XSS위험요소 수

          int numberOfErrors = cr.getNumberOfErrors();
      • 검출된 XSS위험요소에 대한 경고문들

          List<String> errorMessages = cr.getErrorMessages();
      • 검출된 XSS위험요소를 자동제거한 HTML문

          String xssRemovedHTML = cr.getCleanHTML(); 
  • Spring 예제

    1. Service

       @Service
       public class ValidatorForXSS {
      
           private final Logger logger = LoggerFactory.getLogger(this.getClass());
      
           @Value("${customizer.xss.policy.file}")
           String filePathForPolicy = null;
      
           String resourceClassPathForPolicy = "classpath:antisamy-something.xml";
      
           Policy policy = null;
      
           @PostConstruct
           void before(){
               logger.info("======================================== <XSS-POLICY>");
               if (policy == null)
                   policy = tryLoadPolicyFromFileSystem(filePathForPolicy);
               if (policy == null)
                   policy = tryLoadPolicyFromResources(resourceClassPathForPolicy);
               if (policy == null)
                   logger.warn("[XSS-POLICY] Unfortunately Failed to load XSS Policy File.");
               logger.info("======================================== </XSS-POLICY>");
           }
      
           private Policy tryLoadPolicyFromResources(String classPath){
               if (classPath == null || classPath.isEmpty())
                   return null;
      
               Policy policy = null;
               try{
                   ResourceLoader resourceLoader = new DefaultResourceLoader();
                   InputStream policyInputStream = resourceLoader.getResource(classPath).getInputStream();
                   policy = Policy.getInstance(policyInputStream);
      
               }catch(IOException ioe){
                   logger.error("[XSS-POLICY] !CAUTION! Failed to load application resources - " +classPath, ioe);
               }catch(PolicyException pe){
                   logger.error("[XSS-POLICY] !CAUTION! Failed to load wrong file. - " +classPath, pe);
               }
      
               if (policy != null)
                   logger.info("[XSS-POLICY] Complete load policy from application resources - " +classPath);
               return policy;
           }
      
           private Policy tryLoadPolicyFromFileSystem(String filePath){
               if (filePath == null || filePath.isEmpty())
                   return null;
      
               Policy policy = null;
               try{
                   File policyFile = new File(filePath);
                   policy = Policy.getInstance(policyFile);
      
               }catch(Exception e){
                   logger.error("[XSS-POLICY] !CAUTION! Failed to load policy file from file path you defined - " +filePath, e);
               }
      
               if (policy != null)
                   logger.info("[XSS-POLICY] Complete load policy from file system - " +filePath);
               return policy;
           }         
      
           public boolean validateXSS(String html){
               CleanResults cr = analysisHTML(html);
               if (cr != null)
                   return cr.getNumberOfErrors() == 0;
               return true;
           }
      
           public List<String> getXSSErrorMessages(String html){
               CleanResults cr = analysisHTML(html);
               if (cr != null)
                   return cr.getErrorMessages();
               return Collections.emptyList();
           }
      
           public CleanResults analysisHTML(String html) {
               if (policy == null)
                   return null;
               if (html == null || html.isEmpty())
                   return null;
               CleanResults cr = null;
               try{
                   AntiSamy antiSamy = new AntiSamy(policy);
                   cr = antiSamy.scan(html);
               }catch(Exception e){
                   logger.error("[XSS-POLICY] Error ocurred during scanning html.", e);
               }
               return cr;
           }
      
       }
    2. Main

       @Service
       class SomeServiceImpl extends SomeService {
      
           @Autowired
           private ValidatorForXSS validatorForXSS;
      
           public void doSome(String html){
               if (validatorForXSS.validateXSS(html)){
                   throw new Exception("No! XSS!");
               }
           }            
      
       }

5. Reference - 참조