Skip to content

Store CAPTCHA images in the configured media storage (#33051) - #41167

Open
lbajsarowicz wants to merge 2 commits into
magento:2.4-developfrom
lbajsarowicz:fix/33051-captcha-remote-storage
Open

Store CAPTCHA images in the configured media storage (#33051)#41167
lbajsarowicz wants to merge 2 commits into
magento:2.4-developfrom
lbajsarowicz:fix/33051-captcha-remote-storage

Conversation

@lbajsarowicz

Copy link
Copy Markdown
Contributor

Description

With remote storage enabled, the CAPTCHA image is never reachable: the browser gets a 404 and the CAPTCHA cannot be solved. The same happens on a multi-node setup without shared storage, where the node that generates the image is not the node that serves it.

Magento\Captcha\Helper\Data::getImgDir() hardcodes the local driver:

return $this->_filesystem->getDirectoryWrite(DirectoryList::MEDIA, Filesystem\DriverPool::FILE);

so the PNG is written to the local pod's disk. getImgUrl() however builds the URL from $store->getBaseUrl(DirectoryList::MEDIA), and with remote storage that path is proxied to the bucket (see the location /media/ block in nginx.conf.sample). The file and the URL point at two different places. Cron\DeleteExpiredImages pins the local driver in the same way, so it would also never reap anything written remotely.

Fix

The image itself has to be rendered locally — Laminas\Captcha\Image::generateImage() writes it with native imagepng(), which cannot go through a Magento driver at all. So simply dropping DriverPool::FILE does not work: AwsS3::getAbsolutePath() returns a bucket-relative path rather than a stream wrapper, and imagepng() would silently create a bogus local file.

Instead a new afterGenerate plugin on Magento\Captcha\Model\DefaultModel moves the rendered image into the configured media storage. It compares the target media driver with the local File driver and returns untouched when they are the same, so behaviour is unchanged when remote storage is off. This is the same render-locally-then-rename() pattern the existing Magento\RemoteStorage\Plugin\Image uses.

The plugin depends only on Magento\Framework\Filesystem\Directory\TargetDirectory, which is framework-level and is re-pointed at the remote filesystem by RemoteStorage/etc/di.xml, so no new inter-module dependency is introduced.

Cron\DeleteExpiredImages now scans the target media directory as well, so remote objects are actually cleaned up. Its new constructor argument is optional with an ObjectManager fallback, so it is not a breaking change. No signature on Captcha\Helper\Data or DefaultModel was changed.

Fixed Issues

Fixes #33051

Manual testing scenarios

  1. Configure remote storage (remote_storage in app/etc/env.php, AWS S3 driver) and make sure nginx proxies /media/ to the bucket.
  2. Enable CAPTCHA for the customer login form.
  3. Open the login page.
  4. Before: the CAPTCHA image 404s, so the form cannot be submitted. After: the image loads from the bucket and the CAPTCHA can be solved.
  5. Let captcha_delete_expired_images run and confirm the objects are removed from the bucket.
  6. With remote storage disabled, confirm the CAPTCHA still renders exactly as before and the image stays in pub/media/captcha/<website>/.

Questions or comments

Verified locally on 2.4-develop (Warden, PHP 8.3):

  • Unit: Magento/Captcha/Test/Unit/ — 78 tests pass, including three new cases covering the drivers-equal no-op, the move when the drivers differ, and the missing-source-file case.
  • PHPCS Magento2 and PHPStan level 1: clean.
  • Confirmed via bin/magento dev:di:info Magento\Captcha\Model\DefaultModel that the plugin is registered as an after plugin on generate.
  • Smoke-tested the local-storage path end to end: DefaultModel\Interceptor is used, the image is written to pub/media/captcha/base/, and the plugin correctly does nothing.

Two things I could not verify here and would like a reviewer to confirm against a real bucket:

  1. The remote path itself — I have no S3 bucket in this environment, so the rename() across drivers is covered by unit tests only. The MFTF RemoteStorageAwsS3EnabledSuite would be the natural home for end-to-end coverage.
  2. A failed remote PUT currently propagates FileSystemException out of block rendering on the login page. That matches RemoteStorage\Plugin\Image, which also does not catch, but log-and-degrade is a defensible alternative here since the alternative is a broken login form. Happy to change it if maintainers prefer.

Cost note, so it is not a surprise: this adds one remote PUT per CAPTCHA render (and one DELETE from the cron). The read is done by the browser directly from the bucket or CDN, so no PHP-side GET is added.

Contribution checklist

  • Pull request has a meaningful description of its purpose
  • All commits are accompanied by meaningful commit messages
  • All new or changed code is covered with unit/integration tests (if applicable)
  • All automated tests passed successfully (all builds are green)

CAPTCHA images were always written to the local media directory while the
media base URL is served from remote storage, so the browser received a 404
and the CAPTCHA could not be solved. The same happened on multi-node setups.
The image is now moved to the configured media storage after generation, and
the cleanup cron reaps it from there.
@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run all tests

@m2-assistant

m2-assistant Bot commented Aug 30, 2026

Copy link
Copy Markdown

Hi @lbajsarowicz. Thank you for your contribution!
Here are some useful tips on how you can test your changes using Magento test environment.
❗ Automated tests can be triggered manually with an appropriate comment:

  • @magento run all tests - run or re-run all required tests against the PR changes
  • @magento run <test-build(s)> - run or re-run specific test build(s)
    For example: @magento run Unit Tests

<test-build(s)> is a comma-separated list of build names.

Allowed build names are:
  1. Database Compare
  2. Functional Tests CE
  3. Functional Tests EE
  4. Functional Tests B2B
  5. Integration Tests
  6. Magento Health Index
  7. Sample Data Tests CE
  8. Sample Data Tests EE
  9. Sample Data Tests B2B
  10. Static Tests
  11. Unit Tests
  12. WebAPI Tests
  13. Semantic Version Checker

You can find more information about the builds here
ℹ️ Run only required test builds during development. Run all test builds before sending your pull request for review.


For more details, review the Code Contributions documentation.
Join Magento Community Engineering Slack and ask your questions in #github channel.

The build fails on any PHPCS/PHPMD violation in a file the PR touches, so
wrap the two over-long lines in di.xml and suppress the coupling warning
that the new TargetDirectory dependency pushes over the threshold.

Also corrects the import of Filesystem\Io\File, which read
Magento\Captcha\Cron\Magento\Framework\Filesystem\Io\File - a class that
does not exist. Only the constructor docblock referenced it; the signature
uses the fully qualified name, so this was cosmetic.
@lbajsarowicz

Copy link
Copy Markdown
Contributor Author

@magento run Static Tests

@engcom-Bravo engcom-Bravo added the Priority: P2 A defect with this priority could have functionality issues which are not to expectations. label Aug 31, 2026
@github-project-automation github-project-automation Bot moved this to Pending Review in Pull Requests Dashboard Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Priority: P2 A defect with this priority could have functionality issues which are not to expectations. Progress: pending review

Projects

Status: Pending Review

Development

Successfully merging this pull request may close these issues.

Captcha not working with remote storage

2 participants