Skip to content

feat(plugins): support direct download_url in registry entries - #480

Merged
InstaZDLL merged 2 commits into
mainfrom
feat/registry-download-url
Aug 2, 2026
Merged

feat(plugins): support direct download_url in registry entries#480
InstaZDLL merged 2 commits into
mainfrom
feat/registry-download-url

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Adds an optional download_url to a registry entry. When present, install_plugin_from_registry downloads the release asset from that URL instead of the GitHub releases/download URL built from repo.

This decouples the store's install path from GitHub, so the app-controlled registry endpoint (waveflow.app/api/plugins/registry) can host a plugin's binary itself — enabling a closed-source, binary-only plugin with no public source repo (the first consumer being the Spotify Canvas plugin, whose source stays private).

What changes

  • RegistryEntry.download_url: Option<String> (serde default, so every existing entry decodes unchanged; older/GitHub-listed plugins simply omit it).
  • install_plugin_from_registry: if download_url is set, use it (required to be https — no downgrade / loopback target); otherwise the existing github.com/{repo}/releases/download/v{version}/{asset} URL. The downloaded bytes are still blake3-verified against the registry pin either way, so the trust model is unchanged — the registry remains the single source of truth, download_url just moves where the (still-verified) bytes come from.

No schema change to the public waveflow-plugins registry is needed: entries carrying download_url are injected by the app-controlled endpoint, not committed to the GitHub registry (they never pass its CI). Older app builds ignore the field and fall back to the GitHub URL.

Validation

  • cargo check -p waveflow + cargo clippy -p waveflow --all-targets — clean

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • L’installation des plugins prend désormais en charge une URL de téléchargement directe lorsqu’elle est fournie.
    • En l’absence d’URL directe sûre, le téléchargement continue d’utiliser automatiquement la source GitHub configurée.
  • Sécurité

    • Les téléchargements directs sont limités aux adresses HTTPS autorisées.
    • Les redirections sont contrôlées et validées à chaque étape afin de bloquer les destinations non sûres.
    • La vérification d’intégrité BLAKE3 et les contrôles d’installation restent appliqués à tous les plugins.

@InstaZDLL InstaZDLL added scope: backend Rust/Tauri backend (src-tauri/) scope: plugins Plugin runtime, SDK, store, and bundled plugins type: feat New feature size: s 10-50 lines labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Le registre des plugins accepte une URL de téléchargement facultative. L’installation valide cette URL avec le contrôle SSRF partagé. Le téléchargement suit uniquement les redirections validées, avec une limite de dix. L’URL GitHub reste utilisée en l’absence d’URL directe.

Changes

Téléchargement des plugins

Layer / File(s) Summary
Résolution et validation de l’URL
src-tauri/crates/app/src/commands/plugin_store.rs
RegistryEntry ajoute le champ facultatif download_url. L’installation valide cette URL avec le contrôle SSRF. Elle conserve l’URL GitHub comme solution de repli.
Contrôle des redirections
src-tauri/crates/app/src/commands/plugin_store.rs
Le téléchargement suit au maximum dix redirections. Le code valide chaque cible avec le contrôle SSRF avant de la suivre.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RegistryEntry
  participant PluginInstaller
  participant SSRFValidator
  participant DownloadServer
  RegistryEntry->>PluginInstaller: fournir download_url facultatif
  PluginInstaller->>SSRFValidator: valider l’URL
  SSRFValidator-->>PluginInstaller: URL sûre ou refus
  PluginInstaller->>DownloadServer: télécharger l’archive
  DownloadServer-->>PluginInstaller: redirection éventuelle
  PluginInstaller->>SSRFValidator: valider chaque cible
  SSRFValidator-->>PluginInstaller: cible sûre ou refus
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed Le titre est concis, descriptif et respecte le format Conventional Commits avec un scope en kebab-case.
Description check ✅ Passed La description explique clairement le changement, la compatibilité, les contrôles SSRF, la vérification BLAKE3 et les validations exécutées.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/registry-download-url

Comment @coderabbitai help to get the list of available commands.

@InstaZDLL InstaZDLL self-assigned this Aug 2, 2026
@InstaZDLL InstaZDLL added this to the v1.8.0 milestone Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src-tauri/crates/app/src/commands/plugin_store.rs`:
- Around line 494-501: Renforcez la validation de l’URL dans le traitement de
`entry.download_url` : parsez-la, exigez HTTPS, rejetez les hôtes localhost,
loopback, privés et link-local ainsi que les formes d’URL avec identifiants
menant vers une adresse interne, puis validez aussi les adresses DNS résolues.
Dans le client `reqwest` utilisé pour le téléchargement, désactivez les
redirections automatiques ou vérifiez chaque destination avant de la suivre,
notamment les redirections HTTP. Ajoutez des tests couvrant ces URL internes et
les redirections non sûres.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef24e9b5-7a2c-4d23-8372-78fe6a73e552

📥 Commits

Reviewing files that changed from the base of the PR and between de01b35 and cd54eb8.

📒 Files selected for processing (1)
  • src-tauri/crates/app/src/commands/plugin_store.rs

Comment thread src-tauri/crates/app/src/commands/plugin_store.rs
@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai hardened — pushed.

The download_url was only prefix-checked for https://. Now the install download reuses the shared is_safe_motion_url guard (from waveflow_core::artwork::motion_cache) for both the URL and every redirect hop:

  • download_url validation: replaced the starts_with("https://") check with is_safe_motion_url(direct) — requires https AND rejects localhost / loopback / private / link-local / unspecified hosts, including bracketed IPv6 literals, IPv4-mapped/compatible IPv6, and userinfo (user@host) forms pointing at them.
  • Redirect validation: the download reqwest::Client now uses redirect::Policy::custom that re-runs is_safe_motion_url on each hop (capped at 10, matching the default) — a GitHub release URL still follows its 302 to objects.githubusercontent.com (public → allowed), but a redirect to an internal target is refused. This covers HTTP→internal redirects for both the download_url and the existing GitHub path.

Even though the URL is registry-sourced (the trust anchor) and the bytes are blake3-verified, this stops a compromised entry from using the app as an SSRF probe.

Skipped the "resolve DNS and reject internal resolutions" part: same as the motion-cache guard, DNS-rebinding is a documented accepted residual ("a hostname that resolves to an internal IP at connect time … is a residual we accept — plugins are curated and user-installed"). Adding per-hop DNS resolution is out of scope and a deliberate existing trade-off.

On tests: the validation logic IS is_safe_motion_url, which is exhaustively unit-tested in motion_cache (localhost, loopback, private/link-local v4+v6, IPv4-mapped, userinfo, non-https). The install download path itself is network-bound with no unit-test harness, so I didn't add a live-network test there.

Validation: cargo clippy -p waveflow --all-targets clean.

@InstaZDLL InstaZDLL added size: m 50-200 lines and removed size: s 10-50 lines labels Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- Changed files ---'
git diff --name-only HEAD^ HEAD || true

printf '%s\n' '--- plugin_store outline ---'
ast-grep outline src-tauri/crates/app/src/commands/plugin_store.rs --items all --view expanded || true

printf '%s\n' '--- URL guard references and installer context ---'
rg -n -C 8 'is_safe_motion_url|redirect::Policy|install_plugin_from_registry|download_url' \
  src-tauri/crates/app/src/commands/plugin_store.rs src-tauri/crates || true

printf '%s\n' '--- motion_cache outline ---'
guard_file="$(fd -a '^motion_cache\.rs$' src-tauri/crates | head -n 1)"
printf 'guard file: %s\n' "$guard_file"
if [ -n "$guard_file" ]; then
  ast-grep outline "$guard_file" --items all --view expanded || true
  printf '%s\n' '--- URL guard implementation and tests ---'
  rg -n -C 12 'fn is_safe_motion_url|is_safe_motion_url|#\[test\]' "$guard_file" || true
fi

Length of output: 49034


@InstaZDLL La validation SSRF est maintenant appliquée à download_url et à chaque cible de redirection. Cela corrige le problème signalé.

Il reste un défaut mineur dans la limite de redirections. Line 527 utilise attempt.previous().len() > 10. Cette condition autorise une 11e redirection. Si la limite doit rester égale à 10, utilisez >= 10.

if attempt.previous().len() >= 10 {
    attempt.error("too many redirects")
}

✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/480

Timestamp: 2026-08-02T14:16:36.803Z
Learning: Dans `src-tauri/crates/core/src/artwork/motion_cache.rs`, `is_safe_motion_url` est le garde SSRF partagé pour les URL externes. Il impose HTTPS et rejette les hôtes littéraux localhost, loopback, privés, link-local et non spécifiés, y compris les IPv6 avec IPv4 embarquée et les formes avec userinfo. La résolution DNS vers une adresse interne au moment de la connexion reste un risque résiduel accepté pour les plugins organisés et installés par l’utilisateur.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src-tauri/crates/app/src/commands/plugin_store.rs (1)

498-534: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Filtrez les adresses après la résolution DNS.

is_safe_motion_url accepte tout nom d’hôte non local qui n’est pas un littéral IP. Un registre compromis peut donc fournir un nom DNS qui résout vers une adresse privée, link-local ou loopback. reqwest effectue alors une requête interne. Les contrôles des lignes 500 et 529 ont la même limite. Ce cas est une SSRF par résolution DNS stable, et non seulement par DNS rebinding.

Résolvez et filtrez les SocketAddr au moment de la connexion. Ajoutez un test avec un résolveur de test qui retourne une adresse interne. reqwest expose un résolveur personnalisable ; vérifiez l’API exacte dans la version verrouillée avant l’implémentation. (docs.rs)

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- version verrouillée de reqwest ---'
rg -n -C 3 '^name = "reqwest"$|^version = ' Cargo.lock

printf '%s\n' '--- validation URL et résolution DNS ---'
rg -n -C 10 'is_safe_motion_url|dns_resolver|resolve_to_addrs|lookup_host|ToSocketAddrs' \
  src-tauri/crates/core/src/artwork/motion_cache.rs \
  src-tauri/crates/app/src/commands/plugin_store.rs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src-tauri/crates/app/src/commands/plugin_store.rs` around lines 498 - 534,
Update the plugin download client flow around is_safe_motion_url and the reqwest
Client builder to resolve every hostname and reject any resulting private,
loopback, link-local, or otherwise unsafe SocketAddr before connection,
including redirect targets. Use reqwest’s custom Resolve API supported by the
locked version, and add a test with a resolver returning an internal address to
verify the request is refused.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src-tauri/crates/app/src/commands/plugin_store.rs`:
- Around line 498-534: Update the plugin download client flow around
is_safe_motion_url and the reqwest Client builder to resolve every hostname and
reject any resulting private, loopback, link-local, or otherwise unsafe
SocketAddr before connection, including redirect targets. Use reqwest’s custom
Resolve API supported by the locked version, and add a test with a resolver
returning an internal address to verify the request is refused.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4252ba02-dcc1-492c-ae4c-01b0ee70e332

📥 Commits

Reviewing files that changed from the base of the PR and between cd54eb8 and 977635b.

📒 Files selected for processing (1)
  • src-tauri/crates/app/src/commands/plugin_store.rs

@InstaZDLL

Copy link
Copy Markdown
Owner Author

@coderabbitai skipping the custom-Resolve DNS-rebinding hardening, with reasoning.

The download URL comes from the registry — the trust anchor. An attacker who can set download_url has already compromised the catalogue, at which point DNS rebinding buys them nothing extra: they control which plugins are offered. The only marginal capability rebinding adds is a blind SSRF GET to an internal address, and the response is useless — it must match the registry-pinned blake3 to install anything, which an internal service won't produce. So the residual is "an authenticated-by-registry-compromise blind GET", not a code-exec or data-exfil path.

This matches the existing, documented stance of the shared guard: is_safe_motion_url's doc explicitly accepts DNS rebinding as a residual ("a hostname that resolves to an internal IP at connect time … is a residual we accept — plugins are curated and user-installed"). Adding a custom reqwest::dns::Resolve that resolves every hop + rejects internal SocketAddrs (plus a mock-resolver test) is a disproportionate addition for that threat model, and would diverge this one call site from the guard's consistent policy.

What IS in place after this PR: https-only + literal loopback/private/link-local/unspecified rejection (incl. IPv6-mapped + userinfo forms) on the initial URL and re-validated on every redirect hop. That covers the practical SSRF surface for a curated, blake3-verified install path. If we later decide to add resolve-time IP filtering, it should land once in is_safe_motion_url's layer for all callers, not bolted onto this one client.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@InstaZDLL La justification est suffisante. Je retire la remarque sur le filtrage DNS à la résolution.

Le risque résiduel est un GET SSRF aveugle après compromission du registre. Le contenu ne peut pas être installé sans correspondre au hash BLAKE3 épinglé dans le registre. Le garde partagé is_safe_motion_url accepte déjà ce risque pour les plugins organisés et installés par l’utilisateur.

Le filtrage des IP résolues doit rester une amélioration globale de is_safe_motion_url, si le modèle de menace change. Il ne doit pas être ajouté uniquement à ce chemin d’installation.

🐇


✏️ Learnings added
Learnt from: InstaZDLL
URL: https://github.com/InstaZDLL/WaveFlow/pull/480

Timestamp: 2026-08-02T15:44:43.329Z
Learning: Dans `src-tauri/crates/app/src/commands/plugin_store.rs`, les URL `download_url` du registre et chaque cible de redirection sont validées avec le garde SSRF partagé `waveflow_core::artwork::motion_cache::is_safe_motion_url`. La résolution DNS vers une IP interne au moment de la connexion reste un risque résiduel explicitement accepté : une compromission du registre permet au plus un GET SSRF aveugle, et un plugin téléchargé ne peut être installé que si ses octets correspondent au hash BLAKE3 épinglé par le registre. Si un filtrage des IP résolues devient nécessaire, il doit être implémenté dans la couche partagée `is_safe_motion_url` pour tous les appelants.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@InstaZDLL
InstaZDLL merged commit c6d4bc7 into main Aug 2, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/registry-download-url branch August 2, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: plugins Plugin runtime, SDK, store, and bundled plugins size: m 50-200 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant