This episode enters Phase 4 on runtime security: how to load Hermes JavaScript bundles securely, avoid remote code execution and unsafe eval, and practical mitigations for mobile apps and edge runtimes.

In episode 11 we closed Phase 3 by organizing Hermes configuration so builds are reproducible, validated in CI, and integrated with the release pipeline. Now we enter Phase 4: Networking & Security. If performance is about speed, security is about what happens once your app is in the hands of someone with bad intentions.
Episode 12 focuses on secure runtime practices: how your Hermes JavaScript bundle is loaded securely, how to avoid remote code execution and unsafe eval, then real mitigation examples for mobile apps and edge runtimes. The roadmap: the threat model, secure loading, avoiding RCE, mitigations on mobile, then mitigations on the edge.
Hermes executes a bundle compiled into .hbc bytecode. It's important to understand from the start: Hermes bytecode is not encryption. A .hbc file can be unpacked back into disassembly using the hbcdump tool, and its internal structure is documented. What protects the app isn't bytecode "secrecy" but integrity and control over what gets executed.
The typical threat model for a mobile app:
Info
The golden rule: anything you control (bundle, configuration, native code) must always be assumed readable and analyzable by an attacker. Security comes from verification, not from hiding code.
The safest way to load a Hermes bundle is embedding it inside the app assets at build time, not downloading it from a server at runtime. In React Native, the index.android.bundle file (which in a release build is .hbc bytecode) is automatically packed into the APK. As long as you don't replace this mechanism, the bundle is covered by the app's signature in the app store.
If your app is forced to fetch the bundle from the network — for example, for over-the-air updates — never trust the downloaded result blindly. Verify it first against a known hash:
import { checkBundleSignature } from "./crypto-utils";
const EXPECTED_SHA256 = "ab3f...0c9d";
async function loadRemoteBundle(url) {
const bundle = await fetch(url).then((res) => res.arrayBuffer());
const digest = await checkBundleSignature(bundle);
if (digest !== EXPECTED_SHA256) {
throw new Error("Integritas bundel gagal diverifikasi");
}
return bundle;
}A static hash like the one above only makes sense for scheduled releases. For more frequent updates, replace the mechanism with cryptographic signatures: a public key embedded in the app, the bundle signed with a private key on the server, and the app verifying that signature. Always combine this with HTTPS and, where possible, certificate pinning.
The good news: Hermes has never supported local eval from the start. Calling eval in Hermes won't give access to local variables or define new variables outside — the behavior deviates from the spec and in some cases throws an error outright. This isn't a bug, but a design decision that also shrinks the attack surface: there's no free dynamic code execution path.
Even so, you can still shoot yourself in the foot. The dangerous patterns to avoid: new Function, template strings that execute code, and the most classic — fetch then eval. Here's the bad example:
const config = await fetch("https://api.example.com/feature").then((r) => r.text());
const feature = new Function(config);
feature();And here's the safe replacement — data stays data, never becomes code:
const payload = await fetch("https://api.example.com/feature").then((r) => r.json());
const feature = {
enabled: payload.enabled === true,
label: String(payload.label ?? "fitur baru"),
};const code = `return ${userInput};`;
const result = new Function(code)();Note that JSON.parse isn't a one-hundred-percent replacement for every eval case — it only parses data structures. That's exactly the point: if all you need is data, use a data format. If you need logic, write that logic as a real function that takes input, not as a string compiled on the spot.
Warning
Also be careful with helpers that wrap new Function to "build queries" or "template engines". In Hermes, such helpers often break on device due to local eval limitations — besides being a security hazard, they're not portable either.
Here's the minimum set that must always be active for mobile release builds:
evalString provided by some debugging modules).The compiler flags in android/app/build.gradle can also be locked down for consistency across developers:
react {
hermesFlags = ["-O", "-output-source-map"]
}With -output-source-map, production stack traces can be mapped back to the original source — but make sure the resulting .map file is stored in CI for observability tools, not bundled.
In edge runtimes (serverless, Cloudflare Workers, or embedded Hermes in Node.js), the principle is the same, but the threat shifts: now what's untrusted is the request input, not the bundle. The first thought that should come to mind: the code being executed is your own code, while all external input is data. Never build a path where input can become instructions:
import { z } from "zod";
const schema = z.object({
command: z.enum(["list", "detail", "refresh"]),
id: z.string().max(36),
});
export async function handler(req: Request) {
const parsed = schema.safeParse(await req.json());
if (!parsed.success) return new Response("invalid", { status: 400 });
return runKnownCommand(parsed.data);
}With z.enum, the command value is only allowed from a list we know. There's no path where foreign input becomes a dynamically called function name — the pattern still maps strings to already-registered functions, not executing strings.
Hermes runtime security isn't about hiding bytecode, but about controlling what can be executed and ensuring what's executed is genuinely yours. With embedded and verified bundles, a ban on dynamic eval patterns, and strict build keys, the attack surface shrinks drastically.
The essentials to take home:
eval, new Function, and fetch-then-execute patterns in all app code.In episode 13, we continue with Hermes and JIT / AOT Security Considerations: how the AOT choice affects the security profile, what signed bundles and content integrity are, and how to integrate hardening and privacy into the runtime. See you there!