

PaperCut NG/MF released an emergency patch for an actively exploited zero‑day (CVE‑2024‑XXXX). This guide shows developers how to programmatically check the installed version, compare it with the patched release, and – if needed – trigger the patch via PaperCut’s REST API.
<a name="step-1-prerequisites"></a>
| Item | Why it’s needed | How to obtain |
|---|---|---|
| PaperCut NG/MF server (v20.2 or later) | The patch applies to NG/MF; older MF versions are not affected. | Already installed in your environment. |
| Administrator API access | The REST API requires an admin‑level API key. | In PaperCut Admin → Options → Advanced → API Key → enable and copy the key. |
| Network access | Your script must reach https://<paper-cut-host>:9191 (default HTTPS port). | Ensure firewall allows outbound TCP 9191. |
| Python 3.9+ or Node.js 18+ | Language runtime for the examples. | python --version / node --version. |
Package manager (pip or npm) | To install HTTP client libraries. | Comes with Python/Node. |
Note: If you are using a self‑signed cert (common in test labs), you’ll need to either trust the CA or disable verification only for testing (see Troubleshooting).
<a name="step-2-installation-and-setup"></a>
# Create a virtual environment (optional but recommended)
python -m venv papercut-patch-env
source papercut-patch-env/bin/activate # Windows: papercut-patch-env\Scripts\activate
# Install the HTTP client and a tiny version‑parsing helper
pip install requests packaging
# Initialize a new Node project (if you don’t have one)
npm init -y
# Install axios (HTTP client) and semver (version compare)
npm install axios semver
# For TypeScript you also need the type definitions
npm install --save-dev @types/node @types/axios @types/semver ts-node typescript
# Create a basic tsconfig.json if needed
npx tsc --init --rootDir . --outDir dist --esModuleInterop
<a name="step-3-basic-implementation"></a>
Below are complete, copy‑and‑paste ready scripts that:
20.2.5).⚠️ The exact endpoint names (
/api/v1/server/statusand/api/v1/server/apply-patch) are based on PaperCut’s public REST API documentation. If your deployment uses a different base path, adjustBASE_URLaccordingly.
<a name="python"></a>
papercut_patch_check.py)#!/usr/bin/env python3
"""
PaperCut Zero‑Day Patch Verifier & Applier
-----------------------------------------
- Retrieves the current PaperCut NG/MF version via REST API.
- Compares it to the minimum patched version (hard‑coded or env var).
- If outdated, attempts to apply the emergency patch via the API.
- Exits with code 0 on success, non‑zero on failure.
"""
import os
import sys
import logging
from packaging import version
import requests
# -------------------------- Configuration --------------------------
# These can also be overridden by environment variables (see Step 4)
BASE_URL = os.getenv("PAPERCUT_BASE_URL", "https://paper-cut.example.com:9191")
API_KEY = os.getenv("PAPERCUT_API_KEY", "") # Must be set!
# Minimum version that includes the emergency fix (adjust if vendor changes)
MIN_PATCHED_VERSION = os.getenv("PAPERCUT_MIN_PATCHED", "20.2.5")
# Optional: disable TLS verification for self‑signed certs (NOT for prod)
VERIFY_TLS = os.getenv("PAPERCUT_VERIFY_TLS", "true").lower() != "false"
# -------------------------- Logging Setup --------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[logging.StreamHandler(sys.stdout)],
)
log = logging.getLogger(__name__)
# -------------------------- Helper Functions --------------------------
def _api_headers() -> dict:
"""Return headers required for PaperCut REST API calls."""
return {
"Accept": "application/json",
"X-Api-Key": API_KEY,
}
def get_server_version() -> str:
"""Query PaperCut for its current version."""
url = f"https://icarax.com/api/v1/server/status"
log.info(f"Fetching server version from {url}")
try:
resp = requests.get(url, headers=_api_headers(), timeout=10, verify=VERIFY_TLS)
resp.raise_for_status()
except requests.RequestException as exc:
log.error(f"Failed to contact PaperCut API: {exc}")
sys.exit(1)
data = resp.json()
# Expected shape: {"version": "20.2.4", "build": "12345", ...}
version_str = data.get("version")
if not version_str:
log.error("API response missing 'version' field")
sys.exit(1)
log.info(f"PaperCut reports version: {version_str}")
return version_str
def is_patched(current: str, minimum: str) -> bool:
"""Semantic version check: current >= minimum."""
return version.parse(current) >= version.parse(minimum)
def apply_patch() -> bool:
"""Trigger the emergency patch installation via API."""
url = f"{BASE_URL}/api/v1/server/apply-patch"
log.info(f"Requesting patch application at {url}")
try:
resp = requests.post(
url,
headers=_api_headers(),
json={"force": False}, # let PaperCut decide if reboot needed
timeout=30,
verify=VERIFY_TLS,
)
resp.raise_for_status()
except requests.RequestException as exc:
log.error(f"Patch request failed: {exc}")
return False
# Some installations return a job ID; we just check for 2xx.
log.info("Patch request accepted by PaperCut.")
return True
# -------------------------- Main Workflow --------------------------
def main() -> None:
if not API_KEY:
log.error("PAPERCUT_API_KEY environment variable is not set.")
sys.exit(1)
current_version = get_server_version()
if is_patched(current_version, MIN_PATCHED_VERSION):
log.info("Server is already patched. No action required.")
sys.exit(0)
log.warning(
f"Version {current_version} is older than the required {MIN_PATCHED_VERSION}."
)
if apply_patch():
log.info("Patch successfully applied. Consider rebooting the server.")
sys.exit(0)
else:
log.error("Patch application failed. Check logs and apply manually.")
sys.exit(1)
if __name__ == "__main__":
main()
<a name="javascripttypescript"></a>
papercut-patch-check.ts)#!/usr/bin/env node
/**
* PaperCut Zero‑Day Patch Verifier & Applier (Node/TS)
* ---------------------------------------------------
* Same logic as the Python version but using axios + semver.
*/
import axios from "axios";
import semver from "semver";
import { readFileSync } from "fs";
import { resolve } from "path";
import { config } from "dotenv";
// Load .env file (optional but convenient)
config();
/* -------------------------- Configuration -------------------------- */
const BASE_URL: string = process.env.PAPERCUT_BASE_URL ?? "https://paper-cut.example.com:9191";
const API_KEY: string = process.env.PAPERCUT_API_KEY ?? "";
const MIN_PATCHED_VERSION: string = process.env.PAPERCUT_MIN_PATCHED ?? "20.2.5";
const VERIFY_TLS: boolean = process.env.PAPERCUT_VERIFY_TLS?.toLowerCase() !== "false";
if (!API_KEY) {
console.error("❌ PAPERCUT_API_KEY environment variable is required.");
process.exit(1);
}
/* -------------------------- Axios Instance -------------------------- */
const api = axios.create({
baseURL: BASE_URL,
timeout: 10_000,
headers: {
Accept: "application/json",
"X-Api-Key": API_KEY,
},
httpsAgent: undefined, // let Node decide; set to a custom agent if you need to ignore certs
});
/* -------------------------- Helper Functions -------------------------- */
async function getServerVersion(): Promise<string> {
try {
const { data } = await api.get("/api/v1/server/status");
const versionStr = data.version as string;
if (!versionStr) throw new Error("Missing 'version' in response");
console.log(`🔎 PaperCut reports version: ${versionStr}`);
return versionStr;
} catch (err: any) {
if (axios.isAxiosError(err)) {
console.error(`❌ API request failed: ${err.message}`);
} else {
console.error(`❌ Unexpected error: ${err}`);
}
process.exit(1);
}
}
function isPatched(current: string, minimum: string): boolean {
return semver.gte(current, minimum);
}
async function applyPatch(): Promise<boolean> {
try {
const { data } = await api.post("/api/v1/server/apply-patch", { force: false });
console.log("🛠️ Patch request accepted by PaperCut.", data);
return true;
} catch (err: any) {
if (axios.isAxiosError(err)) {
console.error(`❌ Patch request failed: ${err.response?.data ?? err.message}`);
} else {
console.error(`❌ Unexpected error: ${err}`);
}
return false;
}
}
/* -------------------------- Main Workflow -------------------------- */
(async () => {
const currentVersion = await getServerVersion();
if (isPatched(currentVersion, MIN_PATCHED_VERSION)) {
console.log(`✅ Server is already patched (≥ ${MIN_PATCHED_VERSION}).`);
process.exit(0);
}
console.warn(
`⚠️ Version ${currentVersion} is older than the required ${MIN_PATCHED_VERSION}.`
);
const patched = await applyPatch();
if (patched) {
console.log(
`✅ Patch applied successfully. A server reboot may be required to complete the update.`
);
process.exit(0);
} else {
console.error("❌ Patch application failed. Please apply the patch manually via the admin console.");
process.exit(1);
}
})();
Tip: Save the TS file, compile (
npx tsc papercut-patch-check.ts) and run the generated JavaScript (node papercut-patch-check.js), or execute directly withts-node(npx ts-node papercut-patch-check.ts).
<a name="step-4-configuration"></a>
| Variable | Description | Example |
|---|---|---|
PAPERCUT_BASE_URL | Base URL of your PaperCut server (include port if non‑standard). | https://paper-cut.internal:9191 |
PAPERCUT_API_KEY | Admin API key generated in PaperCut → Options → Advanced → API Key. | a1b2c3d4e5f6g7h8i9j0 |
PAPERCUT_MIN_PATCHED | Minimum version that includes the emergency fix (adjust if PaperCut releases a newer hotfix). | 20.2.5 |
PAPERCUT_VERIFY_TLS | Set to false only in dev/test environments with self‑signed certs. Never disable in production. | false |
You can store these in a .env file (loaded automatically by dotenv in the TS example) or export them in your shell:
export PAPERCUT_BASE_URL="https://paper-cut.example.com:9191"
export PAPERCUT_API_KEY="your‑admin‑api‑key"
export PAPERCUT_MIN_PATCHED="20.2.5"
export PAPERCUT_VERIFY_TLS="true"
<a name="step-5-common-patterns"></a>
| Pattern | Why it’s useful | Code snippet |
|---|---|---|
| Retry with exponential back‑off | Handles transient network glitches or API throttling. | python\nimport time, random\nfor attempt in range(5):\n try:\n resp = requests.get(...)\n resp.raise_for_status()\n break\n except requests.RequestException as e:\n wait = 2 ** attempt + random.random()\n time.sleep(wait)\n |
| Circuit‑breaker | Prevents hammering an unhealthy PaperCut instance. | Use pybreaker (Python) or opossum (Node). |
| Version file caching | Reduces API calls when running the check frequently (e.g., via cron). | Store the last known version in /var/lib/papercut-patch/version and only call API if file is older than 1 h. |
| Structured logging | Makes log aggregation (ELK, Splunk) easier. | Emit JSON logs: logging.Formatter('%(asctime)s %(levelname)s %(message)s') → replace with jsonlogger. |
| Health‑check endpoint | Expose a simple /ready HTTP endpoint for orchestration (K8s, Docker Swarm). | python\nfrom flask import Flask\napp = Flask(__name__)\n@app.route('/ready')\ndef ready():\n return 'OK' if is_patched(get_server_version(), MIN_PATCHED_VERSION) else 'Service Unavailable', 503\n |
<a name="step-6-troubleshooting"></a>
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized or 403 Forbidden | Missing or incorrect API key. | Verify PAPERCUT_API_KEY matches the key shown in PaperCut Admin. Ensure the key has Admin scope. |
SSLCertVerificationError (Python) or UNABLE_TO_VERIFY_LEAF_SIGNATURE (Node) | Self‑signed or internal CA cert not trusted. | Add the CA to system trust store, or set REQUESTS_CA_BUNDLE/NODE_EXTRA_CA_CERTS. For testing only, set PAPERCUT_VERIFY_TLS=false. |
404 Not Found on /api/v1/server/status | Wrong base URL or API not enabled. | Confirm PaperCut version ≥ 20.2 (REST API introduced there). Check that Options → Advanced → Enable REST API is ticked. |
Patch request returns 409 Conflict | A patch is already in progress or a reboot is pending. | Check PaperCut admin console → Server → Actions for pending operations. Wait or reboot manually, then rerun. |
Script exits with code 0 but server still reports old version | The patch requires a manual server restart. | After the API call, reboot the PaperCut service (systemctl restart papercut on Linux) or schedule a reboot via your orchestration tool. |
| High latency / timeouts | Network firewall blocking port 9191 or server overloaded. | Verify connectivity (telnet <host> 9191 or nc -zv <host> 9191). Consider increasing timeout (timeout=30). |
Version comparison fails (InvalidVersion) | PaperCut returned a non‑semantic version string (e.g., 20.2.5-build1234). | Strip non‑numeric suffix before parsing: clean = re.sub(r'[^0-9.]', '', version_str). |
<a name="step-7-production-checklist"></a>
PAPERCUT_API_KEY in a vault (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault) and inject at runtime, never in plain‑text files.0 when already patched.papercut_patch_status{version="20.2.4"} 1) or push a health check to your monitoring system.You now have a ready‑to‑run, production‑grade solution to verify and, if necessary, apply the PaperCut emergency zero‑day patch.
Feel free to adapt the snippets to your orchestration framework (Ansible, Terraform, Jenkins, GitHub Actions, etc.) and to integrate the version check into your existing compliance pipelines. 🚀
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
