Repository navigation
fix(createSite): accept remote URLs in build.files again (PHP parity) - #633
Merged
btopro merged 1 commit intoSep 21, 2026
Merged
Conversation
Fixes haxtheweb/issues#3060 for the PHP backend, mirroring the haxcms-nodejs change. praw's backend security/parity rule asks for audit and security issues to be verified and resolved in both backends. createSite has accepted only files staged under <configDirectory>/tmp/imports since the GHSA-q862-gcgq-5m6g hardening, while convertElmslnToSite still returns build.files values as remote URLs, so such an import fails the whole request with 400 "Invalid file import payload in build.files" - after the site directory has already been created. An http(s) value is now fetched through SsrfGuard::safeGuzzleRequest (SSRF-validated, redirects disabled, timeout) into the bulk-import staging root and then takes the path a staged value takes: the isValidBulkImportTmpPath check, then a bulk-import HAXCMSFile::save that validates the content against the extension and records the file in files.json. Everything else - file://, other schemes, relative paths, paths outside the staging root and non-string values - still answers 400, and a URL that cannot be fetched is skipped rather than failing the site. PHP's save() copies rather than moves, so a download is always removed from staging afterwards; the size cap needs no extra work here because PHP already measures a bulk-import source on disk against the site's maxUploadSizeMb. createSite also gains linkImportedPageFiles, the parity counterpart of the Node step from #3043: once the imported files exist it points each page at the file entities its content references, reading identity from files.json through the Entity API, since the pages are written before build.files is ingested. The staging helpers move from systemRoutes/v1/imports/convertHaxcmsToSite.php to lib/stageRemoteFile.php, unchanged, so the importer and createSite share one implementation. Tests: 23 PHPUnit tests mirroring the Node suite - seven for the staging helper and sixteen for importBuildFile and linkImportedPageFiles - including the advisory's payload shape, and private, loopback and metadata addresses refused without any request being sent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes haxtheweb/issues#3060 for the PHP backend — the parity half of the haxcms-nodejs PR for the same issue. praw's backend security/parity rule asks for audit and security issues to be verified and resolved in both backends.
The bug
Since the GHSA-q862-gcgq-5m6g hardening,
createSitehas accepted only files staged under<configDirectory>/tmp/imports, whileconvertElmslnToSitestill returnsbuild.filesvalues as remote URLs. Such an import fails the whole request with400 Invalid file import payload in build.files— after the site directory has already been created.The fix
createSitegainsimportBuildFile(). Anhttp(s)value is fetched throughSsrfGuard::safeGuzzleRequest(SSRF-validated, redirects disabled, timeout) into the bulk-import staging root, and from there every entry takes the path a staged value already takes: theisValidBulkImportTmpPathcheck, then a bulk-importHAXCMSFile::savethat validates the content against the extension and records the file infiles.json.It also gains
linkImportedPageFiles(), the parity counterpart of the Node step from #3043: once the imported files exist, it points each page at the file entities its content references, reading identity fromfiles.jsonthrough the Entity API. The pages are written beforebuild.filesis ingested, so they cannot carry uuids as they are written.The staging helpers move from
systemRoutes/v1/imports/convertHaxcmsToSite.phptolib/stageRemoteFile.php, unchanged, so the importer andcreateSiteshare one implementation.What keeps the advisory closed
http(s)values are fetched, and only throughSsrfGuard, which refuses private, loopback, link-local and cloud-metadata targets and disables redirects.file://, other schemes, relative paths, paths outside the staging root and non-string values (the advisory's{ "tmp_name": … }shape) still answer 400.HAXCMSFile::save: extension allow-list, content-versus-extension check, symlink check.maxUploadSizeMb, so downloads are capped with no extra work.save()copies rather than moves, so a download is always removed from staging afterwards.Tests
tests/Unit/StageRemoteFileTest.php(7) covers the helper: what is staged and how it is named, a staging root that cannot be created, error and empty and failed responses, private and metadata addresses refused without a request being sent, and non-http schemes refused.tests/Unit/CreateSiteBuildFilesTest.php(16) covers the route methods: a URL becoming a file entity, a staged file and a URL together, an upper-case scheme, a key without thefiles/prefix, an extension-less URL (Plone serves images from…/@@images/image), skipped downloads, SSRF refusals, other schemes and unstaged paths rejected, unsafe names rejected before any fetch, a content/extension mismatch dropped, a download over the size limit dropped, and the linking step (page linked to its entity, an image shared by pages, references to files never ingested, a page that arrived without metadata, and one manifest save).vendor/bin/phpunitThe suite also reports 12 failures that are present on untouched
masteron this machine (11InstallerAdvanceTestpassword-policy tests and oneFormatConvertersTestcase). They are unrelated to this change and appear on both trees; this was run on PHP 8.5.4, so they look version-related. Each behaviour above was additionally checked by breaking the code on purpose (8 mutations) and confirming the matching tests fail.🤖 Generated with Claude Code