Invoice/estimate/recurring-invoice creation and update accepted total,
sub_total, tax and due_amount straight from the request with no server-side
recalculation, so a client could persist financial totals that don't match the
line items (and, on invoice update, corrupt the due-amount/paid-amount logic
which keyed off the client total).
- Adds App\Support\DocumentTotals which recomputes item totals (round(price *
quantity) minus per-item discount) and document totals (sub_total, tax with
per-item vs document and tax_included handling, total, due_amount), mirroring
the front-end calculation. Only price/quantity/discounts/tax-line amounts are
trusted.
- getInvoicePayload/getEstimatePayload/getRecurringInvoicePayload override the
client totals with the recomputed values; createItems recomputes each stored
item total; Invoice::updateInvoice keys its due-amount/paid-amount logic off
the recomputed total instead of the request total.
Adds DocumentTotals unit tests + a feature test proving a tampered invoice
total is ignored. Existing create/update tests no longer assert the (now
server-authoritative) derived totals.
gotenberg_host was validated only with Laravel's 'url' rule, which permits
loopback/private/link-local hosts (e.g. http://127.0.0.1, http://10.0.0.1,
the cloud metadata endpoint http://169.254.169.254). When a PDF renders, the
server POSTs the document HTML to that host — an SSRF primitive.
- Adds App\Rules\SafeRemoteUrl: requires http(s) and rejects any host that
resolves to a loopback/private/link-local/CGNAT/reserved address (IPv4 and
IPv6), including literal-IP hosts.
- Wires it into PDFConfigurationRequest for gotenberg_host.
- Adds a defensive re-check in GotenbergPDFDriver before the outbound call to
cover hosts set via env/seed/stale config or DNS rebinding (TOCTOU).
Adds unit tests for the rule + validator integration.
The orderByField/orderBy query params were passed straight into Eloquent's
orderBy() in every model's scopeWhereOrder (and Invoice::scopeApplyFilters),
allowing arbitrary SQL in the ORDER BY clause (boolean-based blind injection).
Adds App\Support\SafeOrderBy::apply() which only accepts a plain, optionally
table-qualified column identifier as the sort target (rejecting expressions,
sub-selects, etc.) and clamps the direction to asc/desc. Routed all 10 model
sort sinks through it. Table-qualified columns stay valid, so joined/aliased
sorts (e.g. estimates by customers.name) are unaffected.
Adds unit tests covering injection rejection, plain + aliased columns, and
direction clamping.
Customer PDF controllers resolved the target document by raw mailable_id,
ignoring mailable_type, and skipped the expiry check on the JSON endpoints.
- Resolve via the $emailLog->mailable morph relation and assert the expected
type (abort 404) so a token issued for one document type can't disclose
another whose numeric id collides.
- Enforce isExpired() (abort 403) on every public path, including the JSON
getInvoice/getEstimate/getPayment endpoints.
- Harden EmailLog::isExpired() to treat a null/unresolvable mailable as
expired instead of throwing.
Adds tests for cross-type 404, JSON-path expiry 403, and the valid path.
- Notes IDOR (GHSA-85wc): NotePolicy::viewNotes/manageNotes now receive the
Note and require hasCompany($note->company_id); NotesController passes the
bound model to authorize() on show/update/destroy.
- Estimate->Invoice IDOR (GHSA-j2vg): ConvertEstimateController authorizes
'view' on the source estimate before creating the invoice.
- User bulk-delete (GHSA-wxrv): UsersController scopes candidate ids via
User::whereCompany() before deletion so cross-company accounts are protected.
Adds feature tests for cross-company 403s plus same-company happy paths.
The v2 self-updater only overlays new files (copyFiles) and never removes
files a release deleted; the one removal path (deleted_files) is not even
sent by the web UI. A major upgrade (v2 → v3) removes thousands of files,
so overlay-only leaves a broken hybrid, and stale bootstrap/cache config +
package-discovery survive and break the new boot.
Backport v3's manifest allow-list approach into this final v2 release:
- Updater::cleanStaleFiles(?string $basePath): delete every file under the
install not listed in the release's manifest.json, except the configured
update_protected_paths. No manifest present → safe no-op (v2→v2 updates).
- Updater::clearCompiledCaches(): wipe bootstrap/cache/*.php and compiled
views so the freshly copied release re-reads config and re-runs package
discovery. Called at the end of copyFiles() — the last point that runs as
the currently-installed code before the new release boots, and necessary
because bootstrap/cache is itself a protected path.
- DeleteFilesController + UpdateCommand: when manifest.json is present, run
cleanStaleFiles(); otherwise fall back to the legacy deleted_files list.
No route or frontend change — both already call the delete step between
copy and migrate.
- config/invoiceshelf.php: add update_protected_paths (.env, storage,
vendor, node_modules, Modules, public/storage, .git, bootstrap/cache,
manifest.json).
The v3 release zip already ships manifest.json (built by its make dist), so
a v2 instance running this updater cleans itself up correctly on upgrade.
Tested: tests/Unit/UpdaterTest.php covers stale removal, protected-path and
manifest preservation, empty-dir pruning, no-manifest no-op, and invalid
manifest. Full suite green.
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). The original fix in 07757e74 only sanitized the Notes field via Invoice/Estimate/Payment::getNotes(), but the same blade templates also render company/billing/shipping address fields with {!! !!} (Blade unescaped output). Those address strings are produced by getCompanyAddress(), getCustomerBillingAddress(), getCustomerShippingAddress() which feed into GeneratesPdfTrait::getFormattedString() — and that method does not call PdfHtmlSanitizer.
Customer-controlled fields (name, street, phone, custom field values) are substituted into address templates via getFieldsArray() without HTML-escaping. A malicious customer name like "Acme <img src='http://attacker/probe'>" therefore reaches Dompdf as raw HTML through the address path, exactly the same CWE-918 SSRF pattern the advisories describe — only blocked today 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 the PdfHtmlSanitizer::sanitize() call 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. The explicit wrapper in each model's getNotes() becomes redundant and is removed (along with the now-unused App\Support\PdfHtmlSanitizer imports). Verified getFormattedString() is only called from PDF code paths (no email body callers, which use strtr() directly) so there is no risk of stripping useful HTML from a non-PDF context.
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.
* Fix CustomerPolicy missing hasCompany() check (cross-company IDOR)
Add $user->hasCompany($customer->company_id) check to view, update,
delete, restore, and forceDelete methods in CustomerPolicy, matching
the pattern used by all other policies (InvoicePolicy, PaymentPolicy,
EstimatePolicy, etc.).
Without this check, a user in Company A with view-customer ability
could access customers belonging to Company B by providing the target
customer's ID.
Add cross-company authorization tests to verify the fix.
Closes#565
* Scope bulk delete to current company to prevent cross-company deletion
Filter customer IDs through whereCompany() before passing to
deleteCustomers(), ensuring users cannot delete customers belonging
to other companies via the bulk delete endpoint.
* feat: Tax included
* Added a toggle switch in tax settings to enable the feature.
* Database migration adding tax_included field into estimates, invoices
and recurring invoices table.
* Toggle switch to enable and store the tax_included by estimates,
invoices and recurring invoices.
* In case of tax included enabled, total taxes will be recalculated and
the invoices, estimates and recurring invoices total won't be sum with
taxes.
* Apply tax included when discount_per_item/tax_per_item item is enabled.
* Custom component to show the net total when tax included is enabled.
* Update invoice and estimates pdfs with net total.
* chore: Tax included by default
A switch button inside the tax settings to enable the tax included by
default in invoices, estimates and recurring invoices.
* Possibility to set a fixed amount on tax types settings
* Pint and manage flat taxes on items
* Fix display errors and handle global taxes
* Tests
* Pint with PHP 8.2 cause with PHP 8.3 version it cause workflow error
* Merging percent and fixed amount into one column
* Now display the currency on SelectTaxPopup on fixed taxes
* Clone estimates
* Clone estimate test feature
* Resolve namespace
* Fix string to int for Carbon
* Fix homes routes and default queue key
* Move dropdown item below View and use the propper translation key
* Convert string references to `::class`
PHP 5.5.9 adds the new static `class` property which provides the fully qualified class name. This is preferred over using strings for class names since the `class` property references are checked by PHP.
* Use Faker methods
Accessing Faker properties was deprecated in Faker 1.14.
* Convert route options to fluent methods
Laravel 8 adopts the tuple syntax for controller actions. Since the old options array is incompatible with this syntax, Shift converted them to use modern, fluent methods.
* Adopt class based routes
* Remove default `app` files
* Shift core files
* Streamline config files
* Set new `ENV` variables
* Default new `bootstrap/app.php`
* Re-register HTTP middleware
* Consolidate service providers
* Re-register service providers
* Re-register routes
* Re-register scheduled commands
* Bump Composer dependencies
* Use `<env>` tags for configuration
`<env>` tags have a lower precedence than system environment variables making it easier to overwrite PHPUnit configuration values in additional environments, such a CI.
Review this blog post for more details on configuration precedence when testing Laravel: https://jasonmccreary.me/articles/laravel-testing-configuration-precedence/
* Adopt anonymous migrations
* Rename `password_resets` table
* Convert `$casts` property to method
* Adopt Laravel type hints
* Mark base controller as `abstract`
* Remove `CreatesApplication` testing trait
* Shift cleanup
* Fix shift first issues
* Updating Rules for laravel 11, sanctum config and pint
* Fix Carbon issue on dashboard
* Temporary fix for tests while migration is issue fixed on laravel side
* Carbon needs numerical values, not strings
* Minimum php version
* Fix domain installation step not fetching the correct company_id
* Fix Role Policy wasn't properly registered
---------
* Create PHP CS Fixer config and add to CI workflow
* Run php cs fixer on project
* Add newline at end of file
* Update to use PHP CS Fixer v3
* Run v3 config on project
* Run seperate config in CI