Skip to content

Instantly share code, notes, and snippets.

@ppkarwasz
Created June 18, 2026 20:13
Show Gist options
  • Select an option

  • Save ppkarwasz/ebbe73834c693eeb46d4ef840c21493f to your computer and use it in GitHub Desktop.

Select an option

Save ppkarwasz/ebbe73834c693eeb46d4ef840c21493f to your computer and use it in GitHub Desktop.
Draft mod_perl threat model

Threat Model: mod_perl 2.0

§1 Header

  • Project: mod_perl 2.0 (mod_perl2), embeds the Perl interpreter into Apache httpd
  • Version binding: 2.000014 (dev), commit ead5012b, 2024-02-06
  • Framework: potiuk gist da14a826283038ddfe38cc9fe6310573
  • Status: draft (simple), 0 maintainer review
  • Provenance legend: (documented) project docs/README · (maintainer) confirmed by maintainer · (inferred) reasoned from code, not yet verified
  • Confidence: ~6 documented / 0 maintainer / ~14 inferred
  • One-paragraph description: mod_perl is an Apache httpd module that embeds a persistent Perl interpreter into the server process. The trusted server operator configures Perl handlers (via Perl*Handler directives, <Perl> config sections, or ModPerl::Registry CGI-style scripts) that run inside each request's lifecycle phases with full server privileges. mod_perl's job is to marshal the Apache request (request_rec, header/env tables, body bucket-brigades) safely into Perl and back.

§2 Scope and Intended Use

Persistent in-process Perl handlers inside Apache httpd, replacing fork-per-request CGI. (documented)

Component family Representative API External touchpoints In model?
Request-data marshaling $r->headers_in, %ENV, $r->subprocess_env HTTP request headers/env Yes
Body/filter I/O $r->read, $filter->read, APR::Brigade HTTP request body Yes
Handler dispatch Perl*Handler, call_sv httpd.conf (operator) Yes (config = trusted)
Registry script runner ModPerl::Registry, PerlRun filesystem (Apache-mapped path) Yes (boundary), No (script contents)
<Perl> config sections Apache2::PerlSections httpd.conf No (operator-only, startup)
Build/glue tooling ModPerl::WrapXS, Apache2::Build, xs/maps build host No

§3 Out of Scope (Non-Goals)

  • The operator's Perl handler code and Registry script contents are fully trusted; their bugs are not mod_perl vulnerabilities. (inferred) This is the standard "runs trusted code by design" carve-out.
  • Perl interpreter and Apache/APR core bugs (upstream, not mod_perl).
  • Non-default RegistryCooker subclasses that re-alias namespace_from => namespace_from_uri.
  • Build/test tooling (ModPerl::*, Apache2::Build, t/).

§4 Trust Boundaries and Data Flow

The boundary is the HTTP request. Everything in request_rec that derives from the client (headers, query string, body, cookies, CGI env) is untrusted; httpd.conf, <Perl> sections, and handler/Registry files are trusted operator inputs. (inferred)

The request can select which configured handler or Apache-mapped file runs (via Apache's normal <Location>/<Directory>/<Files> matching, done before mod_perl runs) but never supplies code. (inferred, confirmed in dispatch review)

§5 Assumptions About Environment

  • Runs inside an Apache MPM (prefork / worker / event); persistent interpreter per process/thread. (documented)
  • Perl built with or without ithreads (PERL_IMPLICIT_SYS); threaded MPMs require a thread-safe Perl. (documented, README)
  • Relies on the APR table invariant: table keys/values are NUL-terminated C strings. (inferred)

§5a Build-Time and Configuration Variants

Knob Default Effect on model Posture
DSO vs static DSO link shape only both supported
Threaded MPM + ithreads Perl platform interpreter pooling (modperl_interp / modperl_tipool) supported
PerlTaintCheck / PerlSwitches -T off taint-tracks request data into handlers operator must enable if handlers touch untrusted data (documented)
Options ExecCGI off required before Registry will compile a file operator-granted (documented, RegistryCooker.pm:258-277)
LimitRequestBody / LimitRequestFields (Apache) Apache defaults bounds body/header sizes mod_perl will marshal mod_perl adds no cap of its own (inferred)

§6 Assumptions About Inputs — per-route trust table

Path / API Source Attacker-controllable? Caller must enforce
%ENV, $r->subprocess_env, $r->headers_in request headers / CGI vars Yes size/count limits via Apache
$r->read($buf, $len) request body Yes (content); $len is handler-chosen don't pass attacker-derived $len unbounded
$filter->read($buf, $len) brigade body Yes same
$r->filename (Registry) URI → Apache translation path is Apache-mapped (trusted-as-resolved) ExecCGI, DocumentRoot hygiene

§7 Adversary Model

Primary: remote, unauthenticated HTTP client sending crafted headers/body/URIs. (inferred) Goal: memory corruption in the C marshaling layer, or code execution via injected bytes.

Out of scope: the server operator, the handler author, co-located processes, anyone who can edit httpd.conf or deploy files (all trusted).

§8 Security Properties the Project Provides

(all inferred from code)

  1. Memory-safe marshaling of request data into Perl, given the APR NUL-termination invariant.
    • Violation symptom: OOB read/write. Severity: security-critical (CVE).
  2. No execution of request-derived bytes as code. Handlers/scripts compile only from operator config and on-disk files; $r is passed as a runtime argument, never concatenated into a compiled string.
    • Violation symptom: RCE. Severity: critical.
  3. Body/filter copies bounded by the caller-requested length, with the destination SV grown to fit before each copy.
    • Violation symptom: heap overflow. Severity: critical.
  4. Registry package names are escaped so a request-mapped path cannot inject Perl syntax into the generated package statement.
    • Violation symptom: code injection. Severity: critical.

§9 Security Properties NOT Provided ("false friends")

  • Does NOT sandbox or limit handler/Registry code. It runs with full server privileges; this is by design, not a weakness. (inferred)
  • Does NOT bound resource use. No mod_perl-side cap on the number/size of CGI vars copied into %ENV, nor on $r->read allocation if a handler passes a large $len. CPU/memory exhaustion driven by handler choices or absent Apache limits is not a mod_perl vulnerability. (inferred)
  • Taint mode is opt-in, not default. mod_perl is not a WAF and does not sanitize request data.

§10 Downstream Responsibilities (operator)

  • Treat all handlers / Registry scripts as trusted code; deploy only what you control.
  • Set Apache LimitRequestBody / LimitRequestFields; mod_perl will not.
  • Enable PerlTaintCheck if handlers process untrusted input.
  • Restrict ExecCGI; never expose Registry over untrusted upload directories.
  • Never feed request bytes into eval / system / backticks in handlers.

§11 Known Misuse Patterns

  • Handler feeding request data into eval / systemBY-DESIGN: property-disclaimed (§9), not a mod_perl bug.
  • Using ModPerl::Registry for untrusted/uploaded scripts → OUT-OF-MODEL: trusted-input (script contents are §3).
  • Relying on mod_perl to cap body/header size → BY-DESIGN (§9); use Apache limits.

§11a Known Non-Findings (Recurring False Positives)

Source review of the three attacker-facing surfaces (request-data marshaling, body/filter I/O, handler dispatch) found no memory-safety or code-execution finding that survives the model. Every dangerous-looking pattern is pre-bounded. These are the scanner false positives most likely to reach a security inbox.

Pattern a scanner flags Why safe under model Cite
strlen(key) / newSVpv(val,0) copying request headers/env into Perl with no length check Length comes from the APR table NUL-termination invariant (§5), not the wire; Perl owns allocation modperl_env.c:37-54,608; modperl_util.c:543
memcpy(&wb->outbuf[outcnt], buf, len) in output path outbuf is a fixed 8 KB array; guarded by flush-if-len+outcnt>sizeof and direct-send-if-len>=sizeof modperl_filter.c:255,262,267
apr_file_read into newSV(size) slurp with no bound size = r->finfo.size (stat, operator filesystem, not request body); post-read finfo.size != size croak catches 32-bit apr_off_t truncation modperl_util.c:616-642
apr_pstrmemdup(pv+bucket->start, bucket->length) using raw bucket length bucket->start + bucket->length validated against actual SV length before use modperl_bucket.c:42-45,77-80
SvGROW + sv_catpvn of body bytes in $filter->read bounded by caller wanted; SV grown to exact new total before each copy; nibble = wanted - SvCUR protected by loop invariant SvCUR <= wanted modperl_filter.c:668-738
eval / eval_pv / call_sv near request handling → "RCE?" compiles only httpd.conf text + on-disk files; $r passed as argument; package names escaped modperl_mgv.c:203; modperl_callback.c:100; RegistryCooker.pm:399-410,680

§12 Conditions That Would Change This Model

  1. namespace_from_uri (RegistryCooker.pm:340-357) is non-default; it feeds sanitized request-URI bytes into the package name. Still escaped and injection-safe, but OUT-OF-MODEL: non-default-build rather than impossible. If it became the default, revise.
  2. The sub {...} anon-handler path string-eval_pvs a directive argument (modperl_mgv.c:203). Operator config today, but any future feature letting request data reach a handler-name string would break §8 property 2. Worth a maintainer question.
  3. New public API, new input format accepted, or a new network surface.

§13 Triage Dispositions

Disposition Meaning Licensed by
VALID Violates a claimed property via in-scope adversary and input §8, §6, §7
VALID-HARDENING No §8 property violated but API enables §11 misuse; project hardens anyway §11
OUT-OF-MODEL: trusted-input Requires attacker control of a trusted parameter (e.g. handler/Registry script contents) §6, §3
OUT-OF-MODEL: adversary-not-in-scope Requires an excluded attacker (operator, co-tenant) §7
OUT-OF-MODEL: unsupported-component In build/test tooling or <Perl>-section startup path §3
OUT-OF-MODEL: non-default-build Only manifests under a discouraged §5a flag (e.g. namespace_from_uri) §5a
BY-DESIGN: property-disclaimed Concerns a property explicitly not provided (sandboxing, resource limits, taint) §9
KNOWN-NON-FINDING Matches a documented recurring false positive §11a
MODEL-GAP Cannot cleanly route to the above triggers §12

§14 Open Questions for Maintainers

Wave 1 (scope and trust):

  1. Confirm handler/Registry script contents are out of scope (trusted operator code) and that mod_perl's contract is safe marshaling, not sandboxing. Proposed answer: yes. → §3, §9
  2. Confirm mod_perl intentionally adds no size/count caps of its own on request headers, env, or body, delegating to Apache (LimitRequestBody etc.). Proposed answer: yes. → §9, §10
  3. Is PerlTaintCheck off-by-default the supported production posture, with taint expected to be enabled when handlers process untrusted input? Proposed answer: yes, operator's choice. → §5a

Wave 2 (edge cases):

  1. Is namespace_from_uri considered a supported configuration, or developer-only? Proposed answer: available but not default; sanitized regardless. → §12
  2. Is the AP_MODE_READBYTES over-read drop in modperl_io_apache.c:316-324 a known correctness limitation? Proposed answer: yes, not a memory-safety issue. → §8

Provenance tags retained per framework. Versioned alongside mod_perl 2.000014; a report against version N is triaged against the model as it stood at N. Living document; revise on the §12 triggers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment