H2O-3 Pre-Auth Insecure Deserialization in Binary Model Import Allows Execution of Attacker-Controlled JAR via Restored custom_metric_func

Project: H2O-3 

Repository: https://github.com/h2oai/h2o-3 

Vulnerability ID: VPLUS-2026-20800

Title: Pre-Authentication Insecure Deserialization in H2O-3 Binary Model Import Leads to Remote Code Execution via custom_metric_func

Description: A critical pre-authentication deserialization vulnerability exists in H2O-3 that allows an unauthenticated attacker to restore a malicious custom_metric_func reference from an imported binary model and trigger execution of attacker-controlled code during scoring.

The attack chain is as follows:

  1. The attacker uploads a malicious JAR into DKV using:
    • POST /3/PutKey.bin
  2. The attacker trains a model with:
    • custom_metric_func=java:<jarKey>=audit.MetricPoc
  3. The model is exported as a binary file using:
    • GET /99/Models.bin/<model>?dir=/tmp/<model>.bin&force=true
  4. The original model is removed.
  5. The binary model is re-imported using:
    • POST /99/Models.bin/

During import, hex.Model.importBinaryModel() performs Keyed.readAll(ab) on attacker-controlled model bytes. This restores the serialized custom_metric_func reference from the binary model.

Later, when the imported model is used for scoring, water.udf.DkvClassLoader resolves the restored java:<jarKey>=<class> reference, loads the attacker-controlled JAR from DKV, and instantiates the specified class. This leads to attacker-controlled code execution inside the server JVM.

The issue was confirmed dynamically. After re-importing the model, scoring was performed without passing custom_metric_func again, yet the response still returned:

  • custom_metric_value=2.0

This value was produced by the malicious class reading the server-side JVM property sys.ai.h2o.audit.stage=2, proving that:

  1. the malicious custom_metric_func reference survived binary export/import, and
  2. the attacker-controlled class was loaded and executed during scoring.

Affected Component: Binary model import / model deserialization / dynamic custom metric loading

Affected File: h2o-core/src/main/java/hex/Model.java

Affected Function: importBinaryModel

Affected Line: 3350

Technical Root Cause: The vulnerability is caused by insecure deserialization of untrusted model bytes during binary model import. The imported object graph can contain dynamic executable references such as custom_metric_func, which are later honored by the scoring pipeline. Because the system also supports loading classes from DKV-backed JARs, the deserialization step effectively revives attacker-controlled code references and enables code execution.

Attack Vector:

POST /3/PutKey.bin                     -> upload malicious JAR
POST /3/ModelBuilders/gbm             -> train model with custom_metric_func=java:<jarKey>=audit.MetricPoc
GET  /99/Models.bin/<model>           -> export binary model
DELETE /3/Models/<model>              -> delete original model
POST /99/Models.bin/                  -> import binary model
POST /3/Predictions/models/<model>/frames/<frame> -> trigger malicious code during scoring

Proof of Concept: The issue was reproduced with the supplied Python script, which:

  • builds a malicious JAR containing audit.MetricPoc
  • uploads it to DKV
  • trains a GBM model with a malicious custom_metric_func
  • exports the model to binary format
  • deletes the original model
  • re-imports the binary model
  • changes a JVM property to a known value
  • performs scoring to verify code execution

Observed Behavior: The reproduced output showed:

  • successful malicious JAR upload
  • successful model training
  • successful binary export and re-import
  • imported model still containing:
    • java:v07_metric_1773964714.jar=audit.MetricPoc
  • scoring result:
    • custom_metric_name = v07_metric_1773964714.jar
    • custom_metric_value = 2.0

This proves that the malicious class reference was restored from the imported model binary and executed during scoring.

Example Verification Output:

IMPORT={
  "model_id": "v07_model_1773964714",
  "imported_custom_metric_func": "java:v07_metric_1773964714.jar=audit.MetricPoc"
}
SCORE={
  "model_id": "v07_model_1773964714",
  "custom_metric_name": "v07_metric_1773964714.jar",
  "custom_metric_value": 2.0,
  "scoring_time": 1773964716977
}

Impact: This vulnerability allows unauthenticated attackers to achieve remote code execution in the H2O-3 server JVM by combining binary model import with DKV-based JAR loading and restored custom_metric_func references. Successful exploitation may lead to full compromise of the application process, access to sensitive data, arbitrary code execution, and potential lateral movement within the environment.

Severity: Critical

CVSS: 9.8

Remediation Recommendations:

  1. Immediately require authentication and strong authorization for high-risk endpoints including /3/PutKey.bin, /99/Models.bin/, model export, import, and scoring-related administrative features.
  2. Do not deserialize untrusted model files using generic Keyed.readAll(...). Replace binary import with a restricted safe format.
  3. Strip, reject, or rebuild executable references such as custom_metric_func during model import.
  4. Enforce strict allowlists or signature verification for JAR keys and classes referenced by custom_metric_func.
  5. Revalidate all imported model parameters after deserialization and reject any dynamic class-loading references.
  6. Consider disabling DKV-backed user-provided code loading by default unless explicitly required in trusted deployments.