Skip to content

fix(createSite): accept remote URLs in build.files again (PHP parity) - #633

Merged
btopro merged 1 commit into
haxtheweb:masterfrom
SanikaA3:fix/3060-createsite-remote-files
Sep 21, 2026
Merged

btopro merged 1 commit into
haxtheweb:masterfrom
SanikaA3:fix/3060-createsite-remote-files

Conversation

@SanikaA3

Copy link
Copy Markdown
Contributor

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, createSite has accepted only files staged under <configDirectory>/tmp/imports, while convertElmslnToSite still returns build.files values as remote URLs. Such an import fails the whole request with 400 Invalid file import payload in build.files — after the site directory has already been created.

The fix

createSite gains importBuildFile(). An http(s) value is fetched through SsrfGuard::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: the isValidBulkImportTmpPath check, then a bulk-import HAXCMSFile::save that validates the content against the extension and records the file in files.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 from files.json through the Entity API. The pages are written before build.files is ingested, so they cannot carry uuids as they are written.

The staging helpers move from systemRoutes/v1/imports/convertHaxcmsToSite.php to lib/stageRemoteFile.php, unchanged, so the importer and createSite share one implementation.

What keeps the advisory closed

  • Only http(s) values are fetched, and only through SsrfGuard, 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.
  • The file is still written by the same validated HAXCMSFile::save: extension allow-list, content-versus-extension check, symlink check.
  • PHP already measures a bulk-import source on disk against the site's maxUploadSizeMb, so downloads are capped with no extra work. save() copies rather than moves, so a download is always removed from staging afterwards.
  • A URL that cannot be fetched — unreachable, blocked or empty — is skipped rather than failing the site.

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 the files/ 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).

suite result
vendor/bin/phpunit 1629 tests, the 23 new ones passing

The suite also reports 12 failures that are present on untouched master on this machine (11 InstallerAdvanceTest password-policy tests and one FormatConvertersTest case). 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

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>
@SanikaA3
SanikaA3 requested a review from btopro as a code owner September 19, 2026 22:24
@btopro
btopro merged commit 7188313 into haxtheweb:master Sep 21, 2026
2 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 21, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createSite rejects build.files entries that are remote URLs, breaking importer image ingestion

2 participants