Skip to content

Contracts / Security Policy

The security-policy domain defines three deny-by-default contracts: CryptoPolicyInterface gates algorithm and key choices, HtmlSecurityPolicyInterface constrains the Hypertext Markup Language (HTML) feature surface, and ExternalResourcePolicyInterface governs remote resource loading. Because each one is a contract, you can supply a stricter deployment policy without forking.

Terminal window
composer require nextpdf/core:^3

CryptoPolicyInterface is the crypto gate. Core calls it before any signing, encryption, or hashing step. The check covers the hash, the signature object identifier (OID), the cipher, and the key strength. The contract also reports the minimum hash and a policy name for the audit log. Use it to apply a rule set such as Federal Information Processing Standards (FIPS) 140-3 or eIDAS. The signing and encryption code does not change. When no policy is set, every algorithm is allowed. A regulated site must set an explicit policy.

HtmlSecurityPolicyInterface works at the HTML parse layer. It runs before content reaches any renderer. It decides whether a tag, an attribute, a Cascading Style Sheets (CSS) property, or a uniform resource locator (URL) scheme is allowed. It also caps input size and nesting depth. It works with the per-renderer transport policies (Chrome, Cloudflare, Gotenberg), which set size limits and Content Security Policy (CSP) headers. The HTML policy reduces the parse-layer attack surface. A stripped tag never reaches layout. An injected element cannot change the output. When no policy is set, the default allows the full feature set.

ExternalResourcePolicyInterface decides whether the HTML pipeline may fetch an external font, stylesheet, or image. It also sets the limits for each fetch. Its default stance is deny-all. Every option is off until you turn it on. The contract follows least privilege. Untrusted HTML may point to an attacker-controlled URL. It gates @font-face fetches by scheme, size, and glyph count. It gates @import by scheme, depth, and total size. It gates background-image by a scheme list and an exact-match domain allowlist. It caps data-URI (Uniform Resource Identifier) size. It also gates Scalable Vector Graphics (SVG) external references. The contract states that production must always deny them. They enable request forgery and script injection. Open URL fetching is a server-side request forgery path. Access control is bypassed by changing the URL, as described by the Open Worldwide Application Security Project (OWASP) Top 10 2025. External component sourcing must be limited to official origins and secure transport.

TypeKindKey membersStabilitySince
CryptoPolicyInterfaceinterfaceisHashAlgorithmAllowed(), isSignatureAlgorithmAllowed(), isEncryptionAlgorithmAllowed(), isKeyStrengthAllowed(), getPreferredHashAlgorithm(), getName()stable1.9.0
HtmlSecurityPolicyInterfaceinterfaceisTagAllowed(), isAttributeAllowed(), isCssPropertyAllowed(), isUrlSchemeAllowed(), getMaxInputSize(), getMaxNestingDepth(), getName()stable3.1.0
ExternalResourcePolicyInterfaceinterfaceisFontFaceAllowed(), getAllowedFontSchemes(), getMaxFontFileSize(), getMaxFontGlyphs(), isImportAllowed(), getMaxImportDepth(), isBackgroundImageAllowed(), getAllowedImageDomains(), getMaxDataUrlSize(), isSvgExternalReferenceAllowed()stable4.0.0

ExternalResourcePolicyInterface returns typed limits: positive-int sizes, int<1, 100> import depth, and list<non-empty-string> scheme and domain lists. The default implementation denies every capability.

examples/contracts/security-policy-quickstart.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\HtmlSecurityPolicyInterface;
/**
* Decide whether a tag survives the policy.
*
* @param HtmlSecurityPolicyInterface $policy A core or custom policy.
*/
function tagSurvives(HtmlSecurityPolicyInterface $policy, string $tag): bool
{
return $policy->isTagAllowed($tag);
}

The function depends only on the contract. Both a restrictive policy and the default policy satisfy it.

examples/contracts/security-policy-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\CryptoPolicyInterface;
use NextPDF\Contracts\ExternalResourcePolicyInterface;
use NextPDF\Contracts\HtmlSecurityPolicyInterface;
use Psr\Log\LoggerInterface;
final readonly class UntrustedHtmlGate
{
public function __construct(
private HtmlSecurityPolicyInterface $htmlPolicy,
private ExternalResourcePolicyInterface $resourcePolicy,
private CryptoPolicyInterface $cryptoPolicy,
private LoggerInterface $logger,
) {}
/**
* Reject input that exceeds the configured limits before rendering.
*
* @param string $html Untrusted HTML markup.
*/
public function assertAcceptable(string $html): void
{
$maxInput = $this->htmlPolicy->getMaxInputSize();
if ($maxInput > 0 && \strlen($html) > $maxInput) {
$this->logger->warning('HTML rejected: input over limit', [
'policy' => $this->htmlPolicy->getName(),
'limit' => $maxInput,
]);
throw new \LengthException('HTML input exceeds policy limit.');
}
if ($this->resourcePolicy->isSvgExternalReferenceAllowed()) {
$this->logger->error('Unsafe policy: SVG external references enabled.');
throw new \LogicException('SVG external references must be denied in production.');
}
}
}

The gate enforces the input cap and rejects an unsafe resource policy before the pipeline runs. It logs the policy name for audit and throws a specific exception.

  • CryptoPolicyInterface allows every algorithm when no policy is set. The open default is a development convenience, not a production posture. Set an explicit policy in any regulated deployment.
  • HtmlSecurityPolicyInterface::getMaxInputSize() returns 0 for unlimited. Treat 0 as “no policy limit”, not “reject all”. Apply a transport-layer cap as well.
  • ExternalResourcePolicyInterface is deny-all by default. Enabling @font-face or background-image without setting a scheme list opens a request-forgery surface; set the allowlist at the same time.
  • An empty domain allowlist on getAllowedImageDomains() means all domains are permitted once background images are enabled. An empty list is not a deny rule; supply explicit domains.
  • isSvgExternalReferenceAllowed() should return false in production. The contract documents this; a policy that returns true is a finding, not a configuration choice.

A policy check is a predicate call: O(1), with no input-proportional cost. The policy is consulted per tag, per attribute, per CSS property, and per URL during parsing. A pathological document multiplies the call count, but each call remains constant time. The performance_budget of 1500 ms wall and 64 MB peak is dominated by parsing and rendering, not by policy evaluation. The input-size and nesting-depth caps bound the parser’s own cost. A strict policy improves worst-case performance by rejecting an oversized or deeply nested document before layout.

These contracts form the engine’s defensive perimeter, so the threat model is explicit. CryptoPolicyInterface mitigates algorithm downgrade by blocking weak hashes and short keys before any operation. HtmlSecurityPolicyInterface mitigates cross-site scripting that affects Portable Document Format (PDF) output and content injection by stripping disallowed tags, attributes, and CSS at the parse layer before the renderer runs. ExternalResourcePolicyInterface mitigates server-side request forgery, decompression bombs, and cumulative-size bombs by defaulting to deny-all and bounding every fetch by scheme, size, depth, and domain. Input-size, nesting-depth, font-glyph, and import-depth caps mitigate resource exhaustion. Because each policy is a contract, you can harden the perimeter without forking the engine, and the policy name is exposed for audit logging. Treat all HTML, all URLs, and all font and image bytes as hostile. This page is marked export_control_class: legal-review-required because the contracts govern cryptographic policy; the prose paraphrases all normative sources and quotes none.

ClaimStandardClauseEvidence
Unconstrained URL handling lets URL changes bypass access control, which the external-resource policy mitigates with deny-all defaults and an exact-match domain allowlist.OWASP Top 10 2025A01
External component sourcing must be limited to official origins and secure transport, which the policy supports through scheme allowlists.OWASP Top 10 2025Software supply chain

Both points paraphrase OWASP guidance. The page references OWASP material by clause; the engine does not reproduce its text.

Core defines and freezes the three policy contracts. It ships permissive defaults for development and strict defaults for the deny-all resource policy. The Enterprise edition ships a FIPS 140-3 profile behind CryptoPolicyInterface, so a regulated deployment gains a validated algorithm posture without changing the signing or encryption code. The contract surface is identical across editions. The difference is the policy implementation you inject.