mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-15 01:04:03 +00:00
Closes the residual surface from the three published SSRF advisories (GHSA-pc5v-8xwc-v9xq, GHSA-38hf-fq8x-q49r, GHSA-q9wx-ggwq-mcgh / CVE-2026-34365 to 34367) that the original 2.2.0 fix only covered for the Notes field. The same blade templates render company/billing/shipping address fields with {!! !!} via Invoice/Estimate/Payment::getCompanyAddress(), getCustomerBillingAddress(), getCustomerShippingAddress() — and those flow through GeneratesPdfTrait::getFormattedString() which did not call PdfHtmlSanitizer.
Customer-controlled fields (name, street, phone, custom-field values) are substituted into address templates via getFieldsArray() without HTML-escaping, so a malicious customer name like "Acme <img src='http://attacker/probe'>" reaches Dompdf as raw HTML through the address path. Today this is blocked only by the secondary defense of dompdf's enable_remote=false; if a self-hoster sets DOMPDF_ENABLE_REMOTE=true for legitimate remote logos, the address surface immediately re-opens.
Move PdfHtmlSanitizer::sanitize() into the chokepoint at GeneratesPdfTrait::getFormattedString() so all four sinks — notes plus the three address fields, on all three models — get the same treatment via a single call site. v3.0's models (Invoice, Estimate, Payment) already had the simpler getNotes() shape (no per-method PdfHtmlSanitizer wrapper), so the trait edit alone is sufficient — no model edits required on this branch. Verified getFormattedString() is only called from PDF code paths (no email body callers, which use strtr() directly).
This is the v3.0 counterpart to master's f387e751. Re-implemented directly on v3.0 instead of cherry-picked because the import-block divergence from the larger v3.0 refactor produced four merge conflicts that were noisier than just porting the chokepoint change manually.
Extends tests/Unit/PdfHtmlSanitizerTest.php with three new cases covering the address-template scenario, iframe/link tag stripping, and on* event handler removal. All 8 tests pass via vendor/bin/pest tests/Unit/PdfHtmlSanitizerTest.php.
73 lines
2.6 KiB
PHP
73 lines
2.6 KiB
PHP
<?php
|
|
|
|
use App\Support\PdfHtmlSanitizer;
|
|
|
|
it('removes img tags that could trigger SSRF during PDF rendering', function () {
|
|
$html = "<p>Hi</p><img src='http://example.com/x'>";
|
|
|
|
expect(PdfHtmlSanitizer::sanitize($html))->not->toContain('<img');
|
|
});
|
|
|
|
it('preserves basic formatting tags', function () {
|
|
$html = '<p><b>Bold</b> and <i>italic</i></p>';
|
|
|
|
expect(PdfHtmlSanitizer::sanitize($html))->toContain('<b>')->toContain('<i>');
|
|
});
|
|
|
|
it('strips style and link attributes that may carry URLs', function () {
|
|
$html = '<p style="background:url(http://example.com/)">x</p><a href="http://evil">y</a>';
|
|
|
|
$out = PdfHtmlSanitizer::sanitize($html);
|
|
|
|
expect($out)->not->toContain('style=')->not->toContain('href=')->not->toContain('example.com');
|
|
});
|
|
|
|
it('returns empty string for empty input', function () {
|
|
expect(PdfHtmlSanitizer::sanitize(''))->toBe('');
|
|
});
|
|
|
|
it('normalizes legacy closing-br markup so lines are not collapsed in PDF output', function () {
|
|
$html = 'line1</br>line2';
|
|
|
|
$out = PdfHtmlSanitizer::sanitize($html);
|
|
|
|
expect($out)->toContain('<br')->toContain('line1')->toContain('line2');
|
|
expect($out)->not->toBe('line1line2');
|
|
});
|
|
|
|
it('strips SSRF vectors injected via address-template placeholders', function () {
|
|
// Simulates the output of GeneratesPdfTrait::getFormattedString() after a
|
|
// malicious customer name like "Acme <img src='http://attacker/probe'>" has
|
|
// been substituted into an address template via {BILLING_ADDRESS_NAME}.
|
|
$html = "Acme <img src='http://attacker.test/probe'><br />123 Main St<br />Springfield";
|
|
|
|
$out = PdfHtmlSanitizer::sanitize($html);
|
|
|
|
expect($out)->not->toContain('<img');
|
|
expect($out)->not->toContain('src=');
|
|
expect($out)->not->toContain('attacker.test');
|
|
expect($out)->toContain('Acme');
|
|
expect($out)->toContain('123 Main St');
|
|
expect($out)->toContain('Springfield');
|
|
});
|
|
|
|
it('strips iframe and link tags that could trigger SSRF', function () {
|
|
$html = '<iframe src="http://attacker/x"></iframe><link rel="stylesheet" href="http://attacker/y.css">Hello';
|
|
|
|
$out = PdfHtmlSanitizer::sanitize($html);
|
|
|
|
expect($out)->not->toContain('<iframe');
|
|
expect($out)->not->toContain('<link');
|
|
expect($out)->not->toContain('attacker');
|
|
expect($out)->toContain('Hello');
|
|
});
|
|
|
|
it('strips on* event handler attributes from allowed tags', function () {
|
|
$html = '<p onload="alert(1)" onclick="x">click me</p>';
|
|
|
|
$out = PdfHtmlSanitizer::sanitize($html);
|
|
|
|
expect($out)->not->toContain('onload')->not->toContain('onclick')->not->toContain('alert');
|
|
expect($out)->toContain('click me');
|
|
});
|