Skip to main content

How V2 anti-hotlink works

WordPress admin → Live2D settings → Basic settings → API type decides where the model loads from and who blocks hotlinking. This page only covers the two modes for the "custom new-version model" (V2 / Cubism 4+): direct loading and host on this site.

V1 models do not go through this page. V1 models are served by the plugin's local REST endpoints (/wp-json/live2d/v1/model/*) and don't need an extra anti-hotlink layer. Everything below is V2 only.

Option A: Direct loading (good for site owners already using object storage / CDN)​

In this mode the plugin doesn't touch your modelAPI — it hands the URL directly to the browser. Hotlink protection is handled by your object storage or CDN. Three common approaches (from simple to complex):

1. Public read + Referer whitelist (simplest)​

  • Aliyun OSS: Bucket → Data security → Anti-hotlink, add your site's domain to the whitelist.
  • Tencent COS: Bucket → Security management → Anti-hotlink, similar setup.
  • How it works: the browser sends a Referer when loading the model; sources outside the whitelist get a 403.
  • Limitation: someone who knows packet capture can spoof Referer to bypass it. Good enough for everyday use.

2. Wrap your own CDN, enable URL signing​

Put Aliyun CDN / Tencent CDN / Cloudflare in front of OSS, enable "URL signing" or "Token validation", then fill modelAPI with the signed URL. Stricter than plain Referer, but more setup.

The bucket is private; every read requires a signature. This mode cannot be configured directly in the plugin's settings page — someone who knows PHP must add code to the theme's functions.php to dynamically generate signed URLs. The plugin does not bundle SDKs for any specific cloud vendor; reasons in the FAQ.

When you pick this, the model files are saved into your own WordPress site (wp-content/plugins/live-2d/model/{slug}/), and the only thing exposed to the public is an encrypted temporary signed URL:

  • What visitors see in F12 is just that temporary URL — the source URL is never leaked.
  • Each file gets an alias (hashed path) that nobody can guess.
  • Starting May 2026, only manual download / upload by the site owner is supported — there is no longer "automatic lazy download on visitor access".

What changes in the form when "Host on this site" is selected​

When this option is on, the modelAPI input auto-hides; the system forces it to your site's wp-content/plugins/live-2d/model/ root. The only thing the site owner needs to fill is the modelDir list — one slug (= subdirectory name) per line. The plugin then expands each into a full manifest URL using the fixed template {root}{slug}/{slug}.model3.json.

For example, to load haru / Pio / shizuku:

modelDir:
- haru
- Pio
- shizuku

The plugin expands them into:

https://your-site.com/wp-content/plugins/live-2d/model/haru/haru.model3.json
https://your-site.com/wp-content/plugins/live-2d/model/Pio/Pio.model3.json
https://your-site.com/wp-content/plugins/live-2d/model/shizuku/shizuku.model3.json

The actual URL a visitor receives looks like /wp-json/live2d/v2/m/{token}/{alias}?e=&s= — a temporary signed link. The source can then be 404'd or hotlink-blocked without affecting anything.

Method 1: Download from URL (model already has a public URL)​

  1. Switch "API type" to Custom new-version model · Host on this site and save.
  2. A "Model hosting" panel appears below, defaulting to the Download from URL tab.
  3. In "Model source URL" paste the model's full public .model3.json URL (e.g. https://example.com/haru/haru.model3.json) and click Download to this site.
  4. The plugin derives a slug from the URL (e.g. haru), immediately appends it to the modelDir list above, and queues all model files; a backend cron pulls them into model/haru/.
  5. Once every job is "Cached", the front-end uses the local copy. Remember to click "Save settings" at the bottom of the page so modelDir is written to the database.

Method 2: Upload a folder (model only exists locally)​

  1. Switch "API type" to Custom new-version model · Host on this site and save.
  2. In the "Model hosting" panel switch to the Upload folder tab. Drag the entire model directory (including .model3.json + .moc3 + textures + motions) into the dashed box, or click Choose folder to pick a directory.
  3. The plugin uploads every file in series to model/{slug}/ (slug auto-derived from the dragged-in root directory name); a progress bar shows uploaded bytes in real time.
  4. After upload, the slug is auto-appended to the modelDir list above.
  5. The yellow banner at the top shows the server's current single-file upload limit (set by PHP upload_max_filesize). Files over the limit are marked red and skipped — the PHP backend won't silently drop them. If your model textures are generally too big, contact your host or raise upload_max_filesize and post_max_size in wp-config.php / .htaccess.
  6. After uploading, click "Save settings" at the bottom so modelDir is written to the database.

Typical scenarios​

  • Models hosted on GitHub Pages / GitHub Raw / jsDelivr.
  • Models hosted on a friend's server where you can't ask them to add a Referer whitelist.
  • Third-party paid models where the seller only gives you a public URL and you don't want to be hotlinked.
  • Worried about object-storage "pay per traffic" bills going out of control.
  • Models exist only locally (no public URL) — just drag and drop.

FAQ​

Why doesn't the plugin connect to OSS private read for me?​

People often ask "why can't the plugin take my OSS AccessKey and sign requests on my behalf?" Three reasons:

  1. AccessKey must not live in the database. WordPress option entries are stored in plaintext. If the database leaks (hack, backup leak, shared-host incident), your entire OSS bucket is gone — far worse than someone hotlinking a model.
  2. Every cloud signs differently. Aliyun, Tencent, AWS, Cloudflare R2, MinIO — each has its own scheme. Done one and someone immediately asks "add XX cloud", endless.
  3. People who actually need this don't need the plugin to help. A site owner who can manage OSS private read can write a few lines of PHP much faster than clicking through a settings page — and it's safer (credentials can come from environment variables instead of the database).

The plugin does not currently provide a hook for you to intercept this flow. If you really want to do it, fork the repo and edit get_asset() / stream_local() in src/live2d-V2Api.php so that hits on your private OSS path return a 302 redirect to a pre-signed OSS URL.

If enough people ask for it, we may add a formal live2d_v2_resolve_alias hook later, but the plugin will never bundle an SDK for any specific cloud vendor.

If you're going down this path, please:

  • Use a least-privilege sub-account (only "Read object", scoped to a specific bucket / directory).
  • Never write the SecretKey directly into a WordPress setting — use environment variables or a dedicated secret manager.
  • Add Referer validation to the V2 anti-hotlink REST path (enable explicitly in wp-config.php; see the comments in live2d-V2Api.php).