9.6. 자바

자바를 사용해서 보안적인 프로그램을 개발하려면 솔직히 자바를 배운 후 첫 단계는 자바 보안에 대한 두 개의 기본 텍스트인 Gong [1999] 와 McGraw [1999] 를 읽는 것이다 (두번째의 경우 특히 7.1 절을 보라). 또한 http://java.sun.com/security/seccodeguide.html 에서 썬이 게시한 보안 코드 지침들을 보아야 한다. 자바의 보안 모델을 설명하는 일련의 슬라이드들은 http://www.dwheeler.com/javasec 에서 얻을 수 있다. 또한 McGraw [1998] 을 볼 수도 있다.

명백히 많은 것들은 개발하고 있는 애플리케이션의 유형에 의존하는데 클라이언트측에서 사용하기 위한 자바 코드는 서버측 코드와는 전혀 다른 환경 (과 신뢰 모델) 을 갖고 있다. 물론 일반적인 원리는 적용된다. 예를 들어 신뢰되지 않은 출처로부터 온 모든 입력을 검사 및 필터링해야 한다. 그러나 자바에는 밑부분에서 논의될 조심해야 할 어떤 숨겨진 입력 또는 잠재적 입력들이 있다. Johnathan Nightingale [2000] 은 자바 프로그래밍에서 많은 문제들을 요약하고 있는 재미있는 문서를 작성했다:

...자바 프로그래밍에서 큰 문제는 상속에 신경을 써야한다는 것이다. 부모, 인터페이스 또는 부모의 인터페이스로부터 메쏘드를 상속한다면 코드에 보안 구멍을 열 것을 각오하는 것이다.

다음은 Gong [1999], McGraw [1999], 썬의 길잡이와 저자의 경험에 기초한 중요한 약간의 지침들이다.

  1. public 필드 또는 변수를 사용하지 마라; 이들은 private 으로 선언하고 이들의 접근성을 제한할 수 있도록 이들에 대한 접근자를 제공해라.

  2. 메쏘드를 private 으로 만들지 않을 확실한 이유가 없다면 private 으로 만들어라 (그렇지 않다면 이를 문서화해라). 이러한 non-private 메쏘드들은 tainted 데이타를 받을 수도 있기 때문에 어쨌든 이들을 보호하기 위해 미리 준비하지 않았다면 그들 자신을 보호해야 한다.

  3. JVM 은 애플릿과는 반대로 애플리케이션에서 런타임시 접근 변경자 (예, private) 를 실제 적용하지 않을 수도 있다. 2000년 11월 7일 "Secure Programming" 메일링 리스트에서 이를 지적한 John Steven (Cigital Inc.) 에 고맙게 생각한다. 문제는 이 모두가 접근을 요청하는 클래스가 어떤 클래스 로더와 함께 적재되느냐에 의존한다는 것이다. 클래스가 신뢰된 클래스 로더 (null/primordial 클래스 로더를 포함해) 와 함께 적재되면 접근 검사는 접근을 허용하는 "TRUE" 를 반환한다. 예를 들어 이는 적어도 썬의 1.2.2 VM 과 함께 작동하는데 그러나 다른 버전에서는 작동하지 않을 수도 있다:

    1. public 필드를 갖는 victim 클래스 (V) 를 작성해 컴파일해라.

    2. 이 필드에 접근하는 attack 클래스 (A) 를 작성해 컴파일해라.

    3. V 의 public 필드를 private 으로 변경한 후 다시 컴파일해라.

    4. A 를 실행시켜라 - 이는 V 의 (지금은 private) 필드에 접근할 것이다.

    그러나 애플릿의 경우는 상황이 다르다. A 를 애플릿으로 변환하고 애플릿뷰어 또는 브라우저로 이를 실행시키면 A 의 클래스 로더는 더이상 신뢰된 (또는 null) 클래스 로더가 아니다. 따라서 코드는 클래스 A 로부터 V.secret 필드를 접근하려 하고 있다는 메시지와 함께 java.lang.IllegralAccessError 를 발생시킬 것이다.

  4. 정적 필드 변수의 사용을 피해라. 이러한 변수는 클래스 (인스턴스가 아닌) 에 소속된 것으로 모든 다른 클래스가 클래스의 위치를 찾을 수 있다. 그 결과 정적 필드 변수는 모든 다른 클래스가 찾을 수 있으며 따라서 정적 필드 변수를 안전하게 하는 것은 더욱 어렵다.

  5. 잠재적으로 악의있는 코드에 가변 객체를 절대로 반환하지 마라 (코드가 이를 변경하기로 결정할 수도 있기 때문에). 배열은 가변적 (물론 배열의 내용들은 가변적이 아닐지라도) 임을 주목해라. 따라서 기밀을 다루는 데이타를 갖는 내부 배열에 대한 참조를 반환하지 마라.

  6. 절대로 사용자가 제공한 가변 객체 (객체의 배열을 포함해) 를 직접적으로 저장하지 마라. 그렇지 않으면 사용자가 객체를 보안적인 코드에 건네주고 보안적인 코드가 객체를 검사하게 한 후 보안적인 코드가 데이타를 사용하려고 하는 동안 데이타를 변경할 수 있다. 배열을 내부적으로 저장하기 전에 복제 (clone) 하고 이에 주의해라 (사용자가 작성한 복제 루틴에 주의해라).

  7. 초기화에 의존하지 마라. 초기화되지 않은 객체를 할당하는 몇몇 방법이 있다.

  8. 확실한 이유가 없다면 모든 것을 final 로 만들어라. 클래스 또는 메쏘드가 final 이 아니라면 공격자가 이를 위험하고 예기치 못한 방식으로 확장하려고 할 수 있다. 물론 이는 보안적이지만 확장성을 잃음을 주목해라.

  9. 보안을 위해 패키지 유효 범위 (scope) 에 의존하지 마라. java.lang 와 같은 약간의 클래스들은 디폴트로 닫혀 있으며 몇몇 JVM (Java Virtual Machine) 은 다른 패키지들을 닫도록 한다. 그렇지 않으면 자바 클래스들은 닫혀있지 않은 상태이다. 따라서 공격자가 패키지안에 새로운 클래스를 도입해서 이를 사용해 보호하고 있다고 생각하는 것에 접근할 수 있다.

  10. 내부 클래스를 사용하지 마라. 내부 클래스가 바이트 코드로 변환될 때 내부 클래스는 패키지 내의 모든 클래스에 접근할 수 있는 클래스로 변환된다. 더욱 바람직하지 않은 것은 내부 클래스를 갖고 있는 (enclosing) 클래스의 private 필드가 소리없이 non-private 가 되어 내부 클래스에 의한 접근을 허용한다.

  11. 권한을 최소화해라. 가능한 어떠한 특별한 허가권도 요구하지 마라. McGraw 는 더 나아가서 모든 코드에 서명하지 말라고 권하고 있다 ; 저자는 더 나아가서 코드에 서명하지만 (따라서 사용자들은 단지 이 전송자 목록에 의해 서명된 코드만 실행시키기로 정할 수 있다) sandbox 권한셋 이상을 필요로 하지 않는 프로그램을 작성하라고 말한다. 더욱 많은 권한을 가져야 한다면 코드를 특별히 엄하게 감사해라.

  12. 코드에 서명해야 한다면 이를 모두 한 아카이브 파일에 놓아라. 여기서 McGraw [1999] 를 인용하는 것이 최선이다.

    이 규칙의 목적은 공격자가 악의있는 클래스와 각자가 서명한 클래스들중 일부를 링크시키거나 절대로 함께 사용되지 않아야 하는 서명된 클래스들을 함께 링크시키는 새로운 애플릿 또는 라이브러리를 구축하는 mix-and-match 공격을 수행하지 못하도록 하는 것이다. 일단의 클래스들을 함께 서명함으로써 이 공격을 더 어렵게 만들어라. 기존의 코드 서명 시스템은 mix-and-match 공격을 예방하기에 부적절한데 따라서 이 규칙이 이러한 공격을 완전히 예방할 수는 없다. 그러나 하나의 아카이브를 사용하는 것이 나쁜 영향을 미칠 수는 없다.

  13. 클래스를 복제 불가능하게 만들어라. 자바의 객체 복제 메카니즘은 공격자가 생성자들 중 어떤 것도 실행시키지 않고도 클래스를 인스턴스화할 수 있도록 한다. 클래스를 불제 불가능하게 만들기 위해서는 단지 각 클래스에 다음 메쏘드를 정의해라:
    public final void clone() throws java.lang.CloneNotSupportedException {
       throw new java.lang.CloneNotSupportedException();
       }

    클래스를 정말 복제가능하게 만들 필요가 있다면 공격자가 클론 메쏘드를 재정의하지 못하도록 취할 수 있는 약간의 보호 조치가 있다. 각자의 클론 메쏘드를 정의한다면 단지 이를 final 로 정의해라. 그렇지 않으면 다음을 추가함으로써 최소한 클론 메쏘드가 악의적으로 재정의되는 것을 예방할 수 있다:
    public final void clone() throws java.lang.CloneNotSupportedException {
      super.clone();
      }

  14. 클래스를 unserializeable 로 만들어라. 직렬화는 공격자가 객체뿐만 아니라 private 부분들의 내부 상태를 볼 수 있도록 한다. 이를 예방하기 위해 클래스에 다음 메쏘드를 추가해라:
    private final void writeObject(ObjectOutputStream out)
      throws java.io.IOException {
         throw new java.io.IOException("Object cannot be serialized");
      }

    직렬화가 무방한 경우라도 반드시 시스템 자원에 대한 직접적인 핸들과 주소 공간에 대한 정보를 포함하는 필드에 대해 transient 키워드를 사용해라. 그렇지 않으면 클래스를 deserializing 함으로써 부적절한 접근을 허용할 수도 있다. 또한 기밀을 다루는 정보를 transient 로 식별하려고 할 수도 있다.

    클래스에 대해 각자의 직렬화 메쏘드를 정의한다면 배열을 취하는 모든 DataInput/DataOutput 에 내부 배열을 전달해서는 안된다. 근본적 이유는 모든 DataInput/DataOutput 메쏘드들은 재정의될 수 있기 때문이다. Serializable 클래스가 DataOutput (write(byte [] b)) 메쏘드에 직접적으로 private 배열을 보낸다면 공격자는 private 배열을 접근 및 변경할 수 있도록 서브 클래스 ObjectOutputStream 을 사용해 write(byte [] b) 메쏘드를 재정의할 수 있다. 디폴트 serialization 은 DataInput/DataOutput 바이트 배열 메쏘드에 private 바이트 배열 필드를 드러내지 않음을 주목해라.

  15. 클래스를 undeserializeable 하게 만들어라. 클래스가 serializeable 하지 않더라도 deserializeable 일 수 있다. 공격자는 자신이 선택한 값을 갖는 클래스의 인스턴스에 우연히 deserialize 할 수 있는 바이트 시퀀스를 생성할 수 있다. 다른 말로 deserialization 은 일종의 public 생성자로 공격자가 객체 상태를 선택할 수 있도록 하는데 이는 명백히 위험한 연산이다. 이를 예방하기 위해 클래스에 다음 메쏘드를 추가해라:
    private final void readObject(ObjectInputStream in)
      throws java.io.IOException {
        throw new java.io.IOException("Class cannot be deserialized");
      }

  16. 이름으로 클래스를 비교하지 마라. 결국 공격자는 동일한 이름을 갖는 클래스를 정의할 수 있으며 주의하지 않는다면 이러한 클래스에 부적당한 권한을 허용함으로써 혼동을 일으킬 수 있다. 다음은 객체가 주어진 클래스를 갖는지를 결정하는데 있어 잘못된 방법의 예이다:
      if (obj.getClass().getName().equals("Foo")) {

    두 객체가 정확히 동일한 클래스를 갖는지 결정할 필요가 있다면 대신 양측에 모두 getClass() 를 사용하고 == 연산자를 사용해 비교해라. 이는 다음과 같이 사용해야 한다:

      if (a.getClass() == b.getClass()) {

    객체가 주어진 클래스이름을 갖는지 정확히 결정할 필요가 있다면 현재 클래스의 ClassLoader 의 현재 이름공간을 사용하는지를 확인할 필요가 있다. 따라서 다음 포맷을 사용할 필요가 있을 것이다:

      if (obj.getClass() == this.getClassLoader().loadClass("Foo")) {

    이 지침은 McGraw 와 Felten 에 의한 것으로 아주 훌륭한 지침인데 저자는 어쨌든 클래스 값들의 비교를 가능한 피하는 것이 언제나 좋은 생각이라고 추가할 것이다. 물론 이를 전혀 할 필요가 없도록 클래스 메쏘드와 인터페이스를 설계하려는 것이 더욱 확실한 방법이다. 그러나 이 방법이 늘 실용적이지는 않으며 따라서 이러한 요령을 아는 것이 중요하다.

  17. 암호화키, 패스워드 또는 알고리듬 같은 비밀을 코드 또는 데이타에 저장하지 마라. 악의가 있는 JVM 들은 빨리 이 데이타를 볼 수 있다. 코드 obfuscation (애매화) 이 실제로 심각한 공격자들로부터 코드를 숨기지는 않는다.