BASIC

Security and Private Storage

This page explains how Hoster protects your packages: where the zips are stored, how client sites get them, and which limits apply.

Private storage

Hoster keeps package zips in its own folder inside uploads:

wp-content/uploads/hoster-private/<random folder>/<file>.zip
  • Each package gets its own folder with a random 32-character name.
  • Hoster writes a deny-all .htaccess file and an index.php into hoster-private/.
  • Zips from Upload new version and the Release API go straight there.
  • A Media Library zip is copied there when you save the Download.
  • Zips rebuilt from private GitHub repositories are written there.
  • Deleting a Download permanently also deletes its private file.

A Download whose package is a URL on another server (for example a public GitHub release asset) has no private copy. Hoster fetches that file and passes it through when a client downloads it.

Apache and LiteSpeed

The .htaccess file blocks the folder, as long as the server allows .htaccess overrides (AllowOverride All, or at least AuthConfig and Limit).

nginx

nginx ignores .htaccess files. Add this rule to the site's server block and reload nginx:

location ^~ /wp-content/uploads/hoster-private/ { deny all; }

If your uploads folder is somewhere else, use its path: the Hoster private storage check in Site Health shows the rule for your site.

Warning
The random folder names make the URLs hard to guess, but they don't replace blocking the folder. Check Tools → Site Health → Hoster private storage after every server move.

The update endpoint

Client sites ask for updates here:

GET https://your-site.com/wp-json/hoster/v1/update/<slug>
  • version — Version installed on the client site.
  • site_url — The client site's URL (home_url()).
  • license_key — The activated licence key, for licensed products.

The answer is JSON with the name, version, requirements, changelog and other sections, banners and icons (or the theme screenshot), and the upgrade notice. It is built on request and sent with Cache-Control: no-store. Only published Downloads answer; an unknown slug gets a 404. A Download that was renamed also answers on its old slug.

For licensed Downloads the answer includes a license object with a status and the message the client shows:

  • valid — no message.
  • missing — "Enter your license key to receive updates."
  • invalid — "Your license key is invalid or not active."
  • expired — "Your license has expired. Renew it to receive updates."
  • site_not_activated — "Your license is not activated for this site."

The package link (download_url) is only included when the status is valid. For free Downloads it is always included.

Signed package links

Packages are served from signed links like this one:

https://your-site.com/hoster-secure-download/?d=123&e=1790000000&l=45&s=…&sig=…
  • The link is signed with a secret that Hoster creates for your site, so it can't be changed or forged.
  • It expires after one hour. The client's updater asks for a fresh link right before it downloads, so the short lifetime doesn't break updates.
  • For licensed Downloads the link is bound to the licence and to the site it was made for.
  • The licence is checked again when the file is downloaded. Setting a licence to expired or invalid, or removing the site from it, stops links that were already handed out.

Change the lifetime with the hoster_package_link_ttl filter (in seconds, at least one minute):

add_filter('hoster_package_link_ttl', function () {
    return 30 * MINUTE_IN_SECONDS;
});

Legacy links

Clients with an older updater (1.x) send no licence key, so their links are not bound to a licence:

  • Updater 1.x clients made with Hoster 1.3 get signed links that last 7 days, because they cache update info for 12 hours. Change that with hoster_legacy_package_link_ttl.
  • Clients generated before Hoster 1.3 read a static wp-content/uploads/downloads/<id>/info.json file. Its package link, like the older /hoster-secure-download/?token=… links, is permanent.

The static files and token links never expire and are not licence-checked. For licensed products they only work while Legacy update links for licensed products is on, so turn that setting off once your customers have moved to Updater 2.1.0. See Migrating to 1.4.0 for those clients.

Download buttons use a different, stable link that checks access when it is clicked. See Download Buttons.

Rate limits

  • Licence checks: after 10 failed attempts from one IP address within a minute, the licence endpoints (activate, deactivate, check) answer with HTTP 429 until the minute is over. Update checks that send a licence key count toward the same limit. Change the number with the hoster_license_max_attempts filter.
  • Package downloads: one IP address can download 1000 packages per day, then gets HTTP 429. Only packages that are actually served count; update checks don't. Change the number with the hoster_max_downloads filter.

Sites behind a proxy or CDN

Behind a proxy or CDN such as Cloudflare, every request comes from the proxy's IP address, so many client sites share one limit. Use the hoster_client_ip filter to read the real visitor IP, for example Cloudflare's CF-Connecting-IP header:

add_filter('hoster_client_ip', function ($ip) {
    return isset($_SERVER['HTTP_CF_CONNECTING_IP'])
        ? sanitize_text_field(wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP']))
        : $ip;
});
Warning
Only use this when the site is really behind that proxy. Otherwise anyone can send the header and pick their own IP address.

Other protections

  • Licence activations compare sites without scheme, www. and trailing slash, see License on Hosted Plugin.
  • The WordPress REST API only shows a Download's raw file URL to users who can edit that Download.
  • Hoster's admin REST routes require an administrator.

Site Health checks

Hoster adds these checks to Tools → Site Health:

  • Hoster update endpoint — Requests the update URL of your most recently changed Download the way a client site does. Fails when a security plugin, firewall, basic auth, maintenance mode or server rule blocks /wp-json/.
  • Hoster private storage — Writes a test file into hoster-private/ and requests it. It should be blocked. When it isn't, the check shows the fix for your server.
  • Hoster links and permalinks — Pretty permalinks are on. With plain permalinks it tests which of Hoster's links still work on your server.
  • Hoster downloads have a package — Lists published Downloads without a zip or download URL.
  • Hoster licence — Hoster's own licence is active.
  • Hoster legacy update links — Whether legacy links are on, and which licensed Downloads older clients asked for in the last 30 days.

Hooks

  • hoster_package_link_ttl (filter) — Lifetime of package links, in seconds (default 1 hour).
  • hoster_legacy_package_link_ttl (filter) — Lifetime of package links for 1.x clients (default 7 days).
  • hoster_client_ip (filter) — IP address used for rate limits.
  • hoster_license_max_attempts (filter) — Failed licence attempts per IP per minute (default 10).
  • hoster_max_downloads (filter) — Package downloads per IP per day (default 1000).
  • hoster_package_served (action) — Runs right before a package is sent, see Download Buttons.
  • hoster_button_no_license_html (filter) — Replaces the locked download button, see Download Buttons.