Study/Effective Java

Finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.

공29 2023. 5. 19. 06:53

교재 : Effective java, 강의 : inflearn 백기선


📎 Effective java 42p

finalizer 공격 원리는 간단하다. 생성자나 직렬화 과정 (readObject와 readResolve 메서드)에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다.

 

Finalizer 공격 예제

accountId가 푸틴일 경우 계정 생성을 막는 Account 클래스 정의

public class Account {

    private String accountId;

    public Account(String accountId) {
        this.accountId = accountId;

        if (accountId.equals("푸틴")) {
            throw new IllegalArgumentException("푸틴 계정을 막습니다.");
        }
    }

    public void transfer(BigDecimal amount, String to) {
        System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
    }

}

 

class AccountTest {

    @Test
    void 일반_계정() {
        // 일반 계정은 돈을 보낼 수 있다.
        Account account = new Account("keesun");
        account.transfer(BigDecimal.valueOf(10.4),"hello");
    }

    @Test
    void 푸틴_계정() {
        // 푸틴 계정은 돈을 보낼 수 없다.
        Account account = new Account("푸틴");
        account.transfer(BigDecimal.valueOf(10.4),"hello");
    }
}

 

Finalizer를 사용하면 푸틴 계정도 돈을 보낼 수 있게끔 할 수 있다.

Account를 상속하면서 finalizer를 오버라이딩하고 transfer를 호출한다.

public class BrokenAccount extends Account {

    public BrokenAccount(String accountId) {
        super(accountId);
    }

    @Override
    protected void finalize() throws Throwable {
        this.transfer(BigDecimal.valueOf(100), "keesun");
    }
}
class AccountTest {
    @Test
    void 푸틴_계정() throws InterruptedException {
        // 푸틴 계정은 돈을 보낼 수 없다.
        Account account = null;
        try {
            account = new BrokenAccount("푸틴");
        } catch (Exception exception) {
            System.out.println("이러면???");
        }

        System.gc();
        Thread.sleep(3000L); // waiting GC...
    }
}

 

출력

이러면???
transfer 100.000000 from 푸틴 to keesun

GC로 인해 finalize가 실행되면서 푸틴 계정이 돈을 보내는 메서드가 호출되었다.

 

Finalizer 공격을 방어하는 방법

1. final 클래스로 만든다.

public final class Account {
    // ...생략
}

누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전하다.

 

2. finalizer() 메서드를 오버라이딩한 다음 final을 붙여서 하위클래스에서 오버라이딩 할 수 없도록 막는다.

public class Account {

    private String accountId;

    public Account(String accountId) {
        this.accountId = accountId;

        if (accountId.equals("푸틴")) {
            throw new IllegalArgumentException("푸틴은 계정을 막습니다.");
        }
    }

    public void transfer(BigDecimal amount, String to) {
        System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
    }

    @Override
    protected final void finalize() throws Throwable {
        // 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.
    }
}

 

하위 클래스에서 finalize를 오버라이딩 할 수 없기 때문에 이 공격을 막을 수 있다.