discovery-media-player 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -360,6 +360,18 @@ function createStandaloneContext(env = process.env) {
360
360
  errors: journal,
361
361
 
362
362
  legal: {
363
+ /**
364
+ * L'URL PUBLIQUE de cette instance, telle que l'exploitant l'a écrite.
365
+ *
366
+ * ⚠️ Sert à fabriquer les liens qui partent par email. L'en-tête `Host` ne convient pas :
367
+ * il est choisi par le client, donc un lecteur pouvait faire envoyer un message signé de
368
+ * l'hôte dont le bouton pointe ailleurs. Vide ⇒ repli sur `Host`, mais signalé.
369
+ */
370
+ get publicUrl() {
371
+ const v = sansBarreFinale(env.PLAYER_PUBLIC_URL).trim();
372
+ return v.startsWith("https://") ? v : "";
373
+ },
374
+
363
375
  get sourceUrl() { return String(env.PLAYER_SOURCE_URL || "https://github.com/Juli1artha/discovery-media-player").trim(); },
364
376
  get legalUrl() { return String(env.PLAYER_LEGAL_URL || "").trim(); },
365
377
  get privacyUrl() { return String(env.PLAYER_PRIVACY_URL || "").trim(); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discovery-media-player",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Self-hosted document viewer: per-recipient tracked links, reading analytics, live presentation. The core knows nothing about the application hosting it — everything it borrows arrives through an injected context.",
5
5
  "keywords": [
6
6
  "pdf-viewer",
package/server/handler.js CHANGED
@@ -49,6 +49,21 @@ const dispositionInline = (name) => {
49
49
  return `inline; filename="${ascii}"; filename*=UTF-8''${encodeURIComponent(raw)}`;
50
50
  };
51
51
  const originOf = (u) => { try { return new URL(u).origin; } catch { return ""; } };
52
+ // ⚠️ CETTE VERSION EST DANS LA PLAGE DE CVE-2024-4367 (exécution de script à l'ouverture d'un PDF
53
+ // forgé, quand `isEvalSupported` garde sa valeur par défaut). Deux raisons pour lesquelles elle
54
+ // est encore là, et il faut les distinguer :
55
+ //
56
+ // 1. Notre CSP n'autorise pas `unsafe-eval`, ce qui bloque le chemin d'exploitation. Mais cette
57
+ // atténuation était IMPLICITE — une modification de CSP la rouvrait sans que rien ne le dise.
58
+ // Les quatre appels à `getDocument` forcent donc désormais `isEvalSupported: false`, en
59
+ // défense en profondeur : la protection ne dépend plus d'un en-tête écrit ailleurs.
60
+ // 2. La montée vers 4.x n'est PAS un changement de numéro : cdnjs ne publie plus que des modules
61
+ // ES (`pdf.min.mjs`) à partir de 4.0, alors qu'on charge un script classique et qu'on
62
+ // configure le worker à la main. C'est une migration, elle est suivie séparément dans
63
+ // docs/AUDIT-2026-08-14-SUIVI.md (P1-3) avec l'embarquement de la bibliothèque, qui règle
64
+ // aussi la dépendance CDN sans intégrité (P2-4).
65
+ //
66
+ // Signalé par un audit externe. Fermer le chemin d'abord, migrer ensuite — dans cet ordre.
52
67
  const PDFJS = "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174";
53
68
  const SUPAJS = "https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2/dist/umd/supabase.js";
54
69
 
@@ -286,7 +301,7 @@ var Live=(function(){
286
301
  function renderMsgInner(m){return Player.chat.renderMessage(m,{me:ME,reactIcon:RSVG});}
287
302
  function cmClass(m){return Player.chat.messageClassName(m,ME,isMentioned(m));}
288
303
  function hydratePdf(d,m){if(m&&m.attachment&&m.attachment.kind==='pdf'&&!m.deleted){var ph=d.querySelector('.cm-att-ph');if(ph)pdfThumb(m.attachment.url,ph);}}
289
- function pdfThumb(url,ph){if(!ph)return;if(pdfCache[url]){ph.innerHTML='<img src="'+pdfCache[url]+'" alt="">';return;}if(!window.pdfjsLib)return;try{pdfjsLib.getDocument(url).promise.then(function(pdf){return pdf.getPage(1);}).then(function(pg){var v0=pg.getViewport({scale:1}),sc=Math.min(1.6,208/v0.width),vp=pg.getViewport({scale:sc}),cv=document.createElement('canvas');cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);return pg.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise.then(function(){var u=cv.toDataURL('image/jpeg',0.8);pdfCache[url]=u;ph.innerHTML='<img src="'+u+'" alt="">';});}).catch(function(){});}catch(e){}}
304
+ function pdfThumb(url,ph){if(!ph)return;if(pdfCache[url]){ph.innerHTML='<img src="'+pdfCache[url]+'" alt="">';return;}if(!window.pdfjsLib)return;try{pdfjsLib.getDocument({url:url,isEvalSupported:false}).promise.then(function(pdf){return pdf.getPage(1);}).then(function(pg){var v0=pg.getViewport({scale:1}),sc=Math.min(1.6,208/v0.width),vp=pg.getViewport({scale:sc}),cv=document.createElement('canvas');cv.width=Math.ceil(vp.width);cv.height=Math.ceil(vp.height);return pg.render({canvasContext:cv.getContext('2d'),viewport:vp}).promise.then(function(){var u=cv.toDataURL('image/jpeg',0.8);pdfCache[url]=u;ph.innerHTML='<img src="'+u+'" alt="">';});}).catch(function(){});}catch(e){}}
290
305
  // Renvoie true seulement si le message a réellement été AJOUTÉ. Pendant la transition, il
291
306
  // arrive par deux voies (diffusion et lecture de table) : sans cette réponse, l'affichage était
292
307
  // bien dédoublonné mais le compteur de non-lus comptait deux fois — une pastille à 2 pour un
@@ -1934,7 +1949,7 @@ ${LEGAL_CSS}
1934
1949
  }
1935
1950
  function render(){
1936
1951
  if(IS_IMG){ renderImage(); return; }
1937
- var task=pdfjsLib.getDocument(CFG.fileUrl);
1952
+ var task=pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false});
1938
1953
  task.onProgress=function(p){ if(p&&p.total){ var pct=Math.max(8,Math.min(99,Math.round(p.loaded/p.total*100))); var bar=document.getElementById('lbar'); if(bar)bar.classList.remove('idle'); var f=document.getElementById('lbarFill'); if(f)f.style.width=pct+'%'; var l=document.getElementById('lpct'); if(l)l.textContent=pct+' %'; } };
1939
1954
  task.promise.then(function(pdf){
1940
1955
  pdfDoc=pdf; numPages=pdf.numPages; window.__n=pdf.numPages; T.setPageCount(pdf.numPages);
@@ -2161,7 +2176,7 @@ function presentHtml(pres, nonce, logoUrl, supaUrl, supaKey) {
2161
2176
  }).catch(function(){ pdfjsLib.GlobalWorkerOptions.workerSrc=wsrc; load(); });
2162
2177
  }
2163
2178
  function load(){
2164
- pdfjsLib.getDocument(CFG.fileUrl).promise.then(function(pdf){
2179
+ pdfjsLib.getDocument({url:CFG.fileUrl,isEvalSupported:false}).promise.then(function(pdf){
2165
2180
  PDF=pdf; total=pdf.numPages; ready=true;
2166
2181
  var tot=document.getElementById('tot'); if(tot)tot.textContent=total;
2167
2182
  show(cur); hideLoader();
@@ -2750,7 +2765,21 @@ async function handler(req, res) {
2750
2765
  refusEnvoi = "no-recipient";
2751
2766
  throw new Error("envoi réservé aux liens nominatifs");
2752
2767
  }
2753
- const origin = `https://${req.headers.host}`;
2768
+ // ⚠️ `Host` EST CHOISI PAR LE CLIENT. Le lien inséré dans un email signé par l'hôte
2769
+ // était construit avec cet en-tête : sur le serveur autonome, ou derrière un proxy qui
2770
+ // ne le réécrit pas strictement, un lecteur pouvait demander un envoi parfaitement
2771
+ // légitime dont le bouton pointe vers SON domaine. L'email part de l'hôte, avec sa
2772
+ // marque et sa réputation, vers le destinataire choisi par l'attaquant.
2773
+ //
2774
+ // `PLAYER_PUBLIC_URL` d'abord — une valeur que l'exploitant a écrite. Le repli sur
2775
+ // `Host` reste pour ne casser aucune instance existante, mais il est SIGNALÉ : une
2776
+ // instance qui envoie des emails sans URL publique configurée doit le savoir avant de
2777
+ // le découvrir dans un rapport d'hameçonnage.
2778
+ const publique = String(PLAYER.legal.publicUrl || "").trim();
2779
+ if (!publique) {
2780
+ try { PLAYER.errors.capture(new Error("PLAYER_PUBLIC_URL non configurée : le lien de l'email est construit depuis l'en-tête Host, que le client choisit"), { route: "reshare" }); } catch { /* jamais bloquant */ }
2781
+ }
2782
+ const origin = publique || `https://${req.headers.host}`;
2754
2783
  const r = await sendReshareEmail({ parent, childSlug: out.slug, origin, toEmail: mail, toName: body.name });
2755
2784
  sent = !!(r && r.sent);
2756
2785
  } catch { /* best-effort : le lien existe quand même */ }