eval 및 new Function과 함께 VM 난독화 사용하기
VM 난독화가 동적 코드 생성(직접 eval과 Function 생성자)을 처리하는 방식, 바이트코드로 변환되는 대상과 건너뛰는 대상, 난독화 도구가 내보내는 경고, 그리고 런타임 ReferenceError를 진단하는 방법을 설명합니다.
이것이 중요한 이유
VM 난독화는 함수 본문을 바이트코드로 컴파일하고, 이 바이트코드는 런타임 내부의 인터프리터를 통해 디스패치됩니다. 주변
스코프의 식별자 이름도 바뀝니다. 두 변환 모두 런타임에 문자열로부터 만들어지는 코드, 즉 eval(s), new Function(...s),
Function(...s)와 잘 맞지 않습니다. 런타임에 만들어진 코드가 난독화 도구가 이름을 바꾼 식별자를 참조하면, 생성된 함수가
처음 실행될 때 Uncaught ReferenceError: <renamed-name> is not defined 오류가 발생합니다.
난독화 도구는 패턴마다 다르게 처리합니다. 아래 표는 요약이며, 이 페이지의 나머지 부분에서 각 행을 자세히 설명합니다.
난독화 도구의 처리 방식 한눈에 보기
| 소스 코드의 패턴 | 결과 |
|---|---|
eval('literal string') (본문이 문자열 리터럴) | 올바르게 실행됩니다. 이 호출을 포함한 함수와 그 안에 정의된 모든 함수는 VM 바이트코드 변환에서 제외됩니다(직접 eval은 주변 지역 변수를 읽는데, 함수가 바이트코드로 컴파일되면 VM은 이 지역 변수를 보존하지 않습니다). |
eval(dynamicExpression) | 런타임에 ReferenceError로 중단될 수 있습니다. 이 호출을 포함한 함수와 그 안에 정의된 모든 함수도 VM 바이트코드 변환에서 제외됩니다. |
(0, eval)(s) / window.eval(s) (간접 호출) | 이 호출을 포함한 함수는 정상적으로 VM 바이트코드로 변환됩니다. 간접 eval은 전역 스코프에서 실행되어 주변 지역 변수를 볼 수 없으므로, 이름이 바뀐 지역 변수 때문에 깨지지 않습니다. 다만 평가되는 코드가 이름이 바뀌었거나 제거된 전역을 참조하면 여전히 실패할 수 있습니다. |
new Function('a', 'b', 'return a + b') (모든 인수가 문자열 리터럴) | 올바르게 실행됩니다. 이 호출을 포함한 함수는 정상적으로 VM 바이트코드로 변환됩니다. |
new Function(dynamicBody) / Function(dynamicBody) | 런타임에 ReferenceError로 중단될 수 있습니다. 이 호출을 포함한 함수와 그 안에 정의된 모든 함수도 VM 바이트코드 변환에서 제외됩니다. |
컴파일러는 eval?.(code)도 보수적으로 감지해 직접 eval처럼 처리합니다. JavaScript는 이 선택적 호출 형태를 간접 eval로
정의하며, 컴파일러의 감지 방식이 이 언어 동작을 바꾸지는 않습니다.
이 패턴 중 일부만 치명적이지 않은 경고를 만들며, 그것도 호출이 함수 안에 있을 때만입니다. 동적 eval, new Function,
Function 호출은 DynamicCodeRenameRisk와 VMDynamicCodeSkipped를 모두 보고하고, 정적 eval('...')은
VMDynamicCodeSkipped만 보고하며, 간접 eval과 완전히 정적인 new Function은 아무것도 보고하지 않습니다. 함수 바깥,
즉 파일의 최상위에 있는 동적 호출은 보고되지 않습니다. 경고 유형과 CI 스니펫은 아래
런타임 전에 문제 감지하기를 참고하세요.
제외는 IIFE를 따라 전파됩니다. 최상위 IIFE가 번들 전체를 감싸고 있고 그 안의 어떤 함수라도 동적 eval이나
new Function을 사용하면, IIFE 전체가 VM 바이트코드 변환에서 제외됩니다. 영향 범위를 줄이는 IIFE 풀기 패턴은
직접 eval 동작 방식을 참고하세요.
정적 코드와 동적 코드를 다르게 처리하는 이유
eval(s)는 자신이 호출된 함수의 지역 변수를 읽고 쓸 수 있습니다. s가 문자열 리터럴이면 난독화 도구는 난독화 시점에
본문을 파싱하여 주변 코드와 일관되게 식별자 이름을 바꿀 수 있습니다. s가 동적 표현식이면 파싱은 런타임에야 일어나는데,
그 시점에는 이미 식별자 이름이 바뀐 뒤이므로 런타임에 만들어진 소스는 더 이상 존재하지 않는 이전 이름을 참조하게 됩니다.
new Function(s)와 간접 eval은 다르게 동작합니다. 본문은 항상 전역 스코프에서 실행되며, 전역 변수에만 접근할 수 있고 호출
주변의 지역 변수에는 절대 접근하지 못합니다. 따라서 이름이 바뀐 지역 변수에 의존할 수는 없지만, 이름이 바뀌었거나 제거된
전역을 참조하면 여전히 실패할 수 있습니다. 이름이 바뀐 식별자를 이어 붙여 본문을 만드는 경우도 마찬가지입니다(예: 난독화
도구가 내부를 재작성한 함수의 func.toString() 사용).
new Function('a', 'b', 'return a + b')처럼 자신의 매개변수만 사용하는 정적 본문에는 이런 의존성이 없습니다. 본문은 이름
변경기가 전혀 검사하지 않는 평범한 문자열입니다. 난독화 도구는 호출을 그대로 두며, 주변 함수는 여전히 VM 바이트코드 변환
대상이 됩니다. eval 문법만 바꾼다고 임의의 동적 코드가 안전해지지는 않습니다. 경고를 검토하고 최종 번들을 테스트하세요.
런타임에 보이는 오류
흔한 증상은 동적으로 만들어진 함수가 처음 실행될 때 발생하는 ReferenceError입니다.
여기서 TU는 난독화 도구가 번들의 IIFE 스코프 안에 도입한, 이름이 바뀐 식별자입니다. 동적 eval 또는 Function 생성자
호출은 이를 참조하는 본문을 평가하지만, 그 본문은 TU가 정의되지 않은 스코프에서 실행됩니다.
런타임 전에 문제 감지하기
난독화 도구는 API를 통해 치명적이지 않은 경고를 내보내므로, 배포 전에 CI에서 이런 패턴 중 일부를 잡아낼 수 있습니다. 관련된 경고 유형은 두 가지이며, 둘 다 함수 안의 호출에 대해서만 보고됩니다.
DynamicCodeRenameRisk- 함수가 런타임에 문자열로부터 코드를 만듭니다. 직접eval, 본문이 정적이지 않은new Function/Function호출, 또는<script>나 Worker에 주입되는fn.toString()이 해당합니다. 이 경고가ReferenceError를 예고하는 경고입니다.VMDynamicCodeSkipped- 함수에 직접eval이나 동적new Function/Function호출이 있어서, 그 함수와 그 안에 정의된 모든 함수의 VM 바이트코드 변환을 건너뛰었습니다. 안전한 정적eval('literal')에서도 발생하므로, 런타임 중단이 아니라 VM 보호를 잃었다는 신호입니다. 함수 이름(있는 경우)과 제외를 유발한 구문이 포함됩니다.
javascript-obfuscator npm 패키지는 결과에서 경고를 제공하지 않으므로, CI 게이트는 API 응답에서 경고를 읽습니다.
result와 chunk_end 메시지에 warnings 배열이 들어 있습니다. 아래 예시는 API 레퍼런스의 스트림 리더인
readObfuscationResponse()를 사용하며, DynamicCodeRenameRisk에 대해서만 빌드를 실패시킵니다. 함수의 VM 보호를 잃는 것도
릴리스를 막아야 한다면 필터에 VMDynamicCodeSkipped를 추가하세요.
빌드에서 예상되는 경고 유형이 있고 이를 CI에서 걸러내기보다 발생 지점에서 끄고 싶다면, warnings 옵션(v7.8.0+)으로
어떤 경고를 내보낼지 제어할 수 있습니다. 'none'은 모든 경고를 억제하고, { VMDynamicCodeSkipped: false }
같은 유형별 맵은 나머지는 유지한 채 한 유형만 끕니다.
해결 방법
간접 eval(
(0, eval)(s))로 바꾸기eval의 경우에만 유효합니다. 간접 eval은 전역 스코프에서 실행되므로 주변 지역 변수를 볼 수 없고, 같은 이유로 이름이 바뀐 지역 변수를 참조할 수도 없습니다. 호출을 포함한 함수는 VM 바이트코드 변환 대상으로 남습니다. 다만 평가되는 코드는 여전히 난독화 도구가 이름을 바꾸는 전역에 의존해서는 안 됩니다.본문을 완전히 정적으로 만들기
new Function의 경우 본문을 보간이 없는 단일 문자열 리터럴이나 템플릿 리터럴로 표현할 수 있다면, 호출에 이름 변경 위험이 없고 이를 감싼 함수도 여전히 바이트코드로 변환됩니다.new Function('a', 'b', 'return a + b')는 괜찮지만new Function('return ' + expr)은 그렇지 않습니다.호출을 별도의 최상위 함수로 옮기고 IIFE 풀기
각 최상위 함수는 독립적으로 검사됩니다. 동적 코드 호출을 별도의 최상위 함수로 빼내면, 번들 전체를 감싼 최상위 IIFE를 따라 제외가 전파되는 대신 그 함수 하나만 VM 바이트코드 변환에서 제외됩니다.
vmTargetFunctionsMode: 'comment'로 전환하기옵트인 모드로,
/* javascript-obfuscator:vm */로 표시한 함수만 바이트코드로 변환됩니다. 동적 코드 호출을 포함한 함수에는 주석을 달지 말고 나머지 함수에 표시하세요. 이 주석은 난독화 단계까지 보존해야 합니다. 함수 지정하기를 참고하세요.vmForceCompileDynamicCode: true로 제외 무시하기 (v6.14.0+)최후의 수단입니다. 이 옵션을 켜면 난독화 도구는 주변 함수를 그대로 바이트코드로 변환하고
VMDynamicCodeSkipped경고를 억제합니다. 스코프나 이름이 바뀐 식별자에 대한 의존성을 고쳐 주지는 않으므로, 런타임에 만들어지는 본문이 난독화 도구가 이름을 바꾸는 식별자를 절대 참조하지 않는다고 보장할 수 있을 때만 사용하세요. 그렇지 않으면 깔끔한 난독화 대신 런타임ReferenceError를 얻게 됩니다.DynamicCodeRenameRisk는 여전히 발생하므로 CI에서 계속 이를 기준으로 빌드를 막을 수 있습니다. 대시보드에서는 VM 섹션의 재정의 그룹에 있는 "Force Compile Dynamic Code" 스위치가 이 옵션입니다.
관련 페이지
- 직접 eval 동작 방식 - 제외의 영향 범위를 줄이는 IIFE 풀기 패턴입니다.
- 함수 지정하기 -
vmTargetFunctionsMode로 함수 단위의 옵트인 또는 옵트아웃을 설정합니다.
