백엔드에서 바이트코드 배열 키 가져오기
VM 바이트코드 복호화 키를 클라이언트에 두지 마세요. 비동기 키 게터로 로드 시점에 백엔드에서 키를 가져오고 인증 뒤에 감춰 두면, 번들을 탈취당해도 무용지물이 됩니다.
문제
vmBytecodeArrayEncoding은 VM 바이트코드를 키로 암호화합니다. 그 키가 번들 안에 함께
포함되어 배포되면, 파일을 가진 사람은 누구나 복호화에 필요한 모든 것을 손에 넣게 됩니다. vmBytecodeArrayEncodingKeyGetter를
사용하면 키를 다른 곳에 두고 런타임에 만들어 낼 수 있지만, 동기 게터는 이미 클라이언트에 존재하는 값(전역 변수, 쿠키,
localStorage)만 읽을 수 있습니다. 서버에서 키를 가져와 인증 뒤에 감추고 필요할 때 폐기하려면 게터가 비동기여야 합니다.
해결책
vmAsyncExecutor(v7.3.0+)를 활성화하세요. 비동기 실행기를 사용하면 키 게터가
Promise를 반환할 수 있으므로, VM이 실행되기 전에 백엔드에서 키를 fetch할 수 있습니다. 다음 세 옵션이 함께 동작합니다.
vmBytecodeArrayEncoding: true— 바이트코드 배열을 암호화합니다.vmBytecodeArrayEncodingKey— 컴파일 시점에 사용하는 키입니다(번들이 아니라 서버에 보관합니다).vmBytecodeArrayEncodingKeyGetter— 런타임에 같은 키를 반환하는 JS 표현식입니다.vmAsyncExecutor가 켜져 있으면Promise를 반환할 수 있고, 그렇지 않으면 게터가 키를 동기적으로 반환해야 합니다.
클라이언트 난독화 설정
JavaScriptObfuscator.obfuscate(sourceCode, {
vmObfuscation: true,
vmAsyncExecutor: true,
vmBytecodeArrayEncoding: true,
vmBytecodeArrayEncodingKey: process.env.VM_KEY, // e.g. 'mySecretKey123'
vmBytecodeArrayEncodingKeyGetter:
'fetch("/api/vm-key", { credentials: "include" }).then((res) => res.text())'
});
게터 표현식은 그대로 삽입되어 브라우저에서 평가됩니다. 컴파일 시점의 vmBytecodeArrayEncodingKey는 클라이언트 저장소에 두지
마세요. 빌드 시점에 환경 변수나 시크릿으로 주입하고, 아래 엔드포인트에서 정확히 같은 문자열을 내려주면 됩니다.
두 키가 동작하는 방식
여러분의 키는 단독으로 사용되지 않습니다. 양쪽 모두에서 난독화 도구가 관리하는 내부 키와 섞입니다.
- 컴파일 시점.
vmBytecodeArrayEncodingKey는 난독화 도구가 생성한 내부 키와 결합되고, 그 결과로 만들어진 혼합 키로 바이트코드 배열이 인코딩됩니다. - 런타임.
vmBytecodeArrayEncodingKeyGetter가 해석해 낸 값, 즉 서버가 내려준 값이 여러 런타임 요소로부터 클라이언트에서 재구성된 동일한 내부 키와 결합되어 바이트코드를 디코딩합니다.
양쪽 모두 여러분의 키를 내부 키와 섞기 때문에, 게터는 vmBytecodeArrayEncodingKey로 전달한 것과 완전히 동일한 문자열을
반환해야 합니다. 어느 한쪽만으로는 충분하지 않습니다. 내부 키 없이 여러분의 키만으로는 바이트코드를 디코딩할 수 없고, 내부
키도 여러분의 키 없이는 쓸모가 없습니다. 인증된 호출자에게만 키를 내려주면 번들을 탈취당해도 무용지물이 되는 이유가 바로
여기에 있습니다.
서버 측
이 엔드포인트는 애플리케이션이 신뢰하는 기준, 예컨대 유효한 세션, 예상되는 Origin 또는 Referer, 라이선스 확인 등에 따라
어떤 키를 반환할지 결정합니다. 여기에 한 가지 반전이 있습니다. 신뢰할 수 없는 호출자를 거부하는 대신 잘못된 키를
반환하는 것입니다. 그러면 바이트코드가 쓰레기 값으로 디코딩되어 보호된 코드가 알아서 실패하는데, 공격자에게 무엇을
우회해야 하는지 그대로 알려주는 노골적인 401보다 훨씬 눈에 덜 띕니다.
// Express example — the exact checks depend on your app
app.get('/api/vm-key', (req, res) => {
const origin = req.get('origin');
const trusted =
req.session?.user && // a valid session, and
origin === 'https://app.example.com'; // the expected production origin
res.type('text/plain').send(
// Real key for valid users; a decoy for everyone else
// (no session, or a localhost / unexpected origin).
trusted ? process.env.VM_KEY : process.env.VM_DECOY_KEY
);
});
