discovery-media-player 0.1.17 → 0.1.19
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.
- package/context/storage.js +45 -4
- package/package.json +1 -1
- package/server/handler.js +45 -4
package/context/storage.js
CHANGED
|
@@ -218,15 +218,56 @@ function isAllowedStorageUrl(candidate, origins, hostBase, root) {
|
|
|
218
218
|
* ⚠️ Le secret n'accompagne QUE la route de l'hôte. L'envoyer à un Storage public le ferait fuiter
|
|
219
219
|
* dans les journaux d'un tiers, où il n'a rien à faire.
|
|
220
220
|
*/
|
|
221
|
+
// ⚠️ `fetch` SUIT LES REDIRECTIONS PAR DÉFAUT, ET N'EN REVALIDE AUCUNE.
|
|
222
|
+
//
|
|
223
|
+
// La garde ci-dessus n'examinait que l'URL de DÉPART. Un amont autorisé — la route de l'hôte, ou
|
|
224
|
+
// n'importe quelle origine de Storage listée — qui répond `302` emmenait donc l'appel où il
|
|
225
|
+
// voulait : `localhost`, une adresse privée, une API de métadonnées cloud. L'invariant annoncé
|
|
226
|
+
// dans le README (« no redirect following into your private network ») était faux.
|
|
227
|
+
//
|
|
228
|
+
// ⚠️ ET LE SECRET SUIVAIT. `fetch` ne retire que `Authorization`, `Cookie` et
|
|
229
|
+
// `Proxy-Authorization` lors d'une redirection inter-origines — un en-tête maison comme
|
|
230
|
+
// `x-player-fetch-secret` est transmis tel quel. Mesuré avec deux serveurs locaux avant de
|
|
231
|
+
// corriger : la destination recevait le secret partagé de l'hôte en clair. Ce n'est donc pas
|
|
232
|
+
// seulement une SSRF, c'est une exfiltration de la clé qui autorise à lire TOUS ses documents.
|
|
233
|
+
//
|
|
234
|
+
// On suit donc les sauts à la main. Trois propriétés, et chacune ferme une porte différente :
|
|
235
|
+
// 1. chaque saut repasse la garde complète — une redirection n'ouvre rien que l'URL de départ
|
|
236
|
+
// n'aurait pas ouvert ;
|
|
237
|
+
// 2. le secret est recalculé À CHAQUE SAUT — il ne part que si CE saut-là est sous la route de
|
|
238
|
+
// l'hôte, jamais parce que le premier l'était ;
|
|
239
|
+
// 3. le nombre de sauts est borné, et le protocole ne peut pas changer de nature : une
|
|
240
|
+
// redirection vers `file:` transformerait un amont distant en lecture de disque local.
|
|
241
|
+
const MAX_REDIRECTIONS = 5;
|
|
242
|
+
const DELAI_MAX_MS = 60_000;
|
|
243
|
+
|
|
221
244
|
async function fetchAllowedFile(url, { range } = {}, { origins, hostBase, root, secret } = {}) {
|
|
222
245
|
if (!isAllowedStorageUrl(url, origins || [], hostBase, root)) return null;
|
|
223
246
|
const local = root && resolveLocal(url, root);
|
|
224
247
|
if (local) return readLocal(local, range);
|
|
225
248
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
249
|
+
let cible = String(url);
|
|
250
|
+
for (let saut = 0; saut <= MAX_REDIRECTIONS; saut++) {
|
|
251
|
+
const headers = { "accept-encoding": "identity" };
|
|
252
|
+
if (range) headers.range = range;
|
|
253
|
+
// Recalculé à chaque tour : c'est CE saut-ci qui doit être sous la route de l'hôte.
|
|
254
|
+
if (isHostFetchUrl(cible, hostBase) && secret) headers["x-player-fetch-secret"] = secret;
|
|
255
|
+
|
|
256
|
+
// Un amont qui ne répond jamais immobiliserait la requête et ses ressources indéfiniment.
|
|
257
|
+
// Le délai est large — un gros document met du temps — mais il est borné.
|
|
258
|
+
const r = await fetch(cible, { headers, redirect: "manual", signal: AbortSignal.timeout(DELAI_MAX_MS) });
|
|
259
|
+
if (r.status < 300 || r.status > 399) return r;
|
|
260
|
+
|
|
261
|
+
const suivante = r.headers.get("location");
|
|
262
|
+
if (!suivante) return r; // 3xx sans destination : on rend la réponse telle quelle
|
|
263
|
+
let absolue;
|
|
264
|
+
try { absolue = new URL(suivante, cible); } catch { return null; }
|
|
265
|
+
// ⚠️ Jamais de changement de nature : `file:` ferait d'un amont distant une lecture de disque.
|
|
266
|
+
if (absolue.protocol !== "https:" && absolue.protocol !== "http:") return null;
|
|
267
|
+
if (!isAllowedStorageUrl(absolue.href, origins || [], hostBase, root)) return null;
|
|
268
|
+
cible = absolue.href;
|
|
269
|
+
}
|
|
270
|
+
return null; // boucle de redirections : on refuse plutôt que de tourner
|
|
230
271
|
}
|
|
231
272
|
|
|
232
273
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discovery-media-player",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
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
|
@@ -399,12 +399,53 @@ var Live=(function(){
|
|
|
399
399
|
ch.on('presence',{event:'sync'},function(){renderPres(ch.presenceState());});
|
|
400
400
|
// Plus d'abonnement à la table des messages : elle n'est plus publiée ni lisible
|
|
401
401
|
// publiquement. Tout passe par la diffusion, et l'historique par la route de chat.
|
|
402
|
-
|
|
403
|
-
|
|
402
|
+
// ⚠️ UNE DIFFUSION EST UN SIGNAL, PAS UNE VÉRITÉ.
|
|
403
|
+
//
|
|
404
|
+
// Ce canal est PUBLIC : la clé publiable et le slug sont dans la page, donc tout participant
|
|
405
|
+
// peut émettre. Appliquer directement la charge utile revenait à laisser n'importe quel
|
|
406
|
+
// spectateur annoncer la fin de la présentation, changer la page affichée, verrouiller le
|
|
407
|
+
// chat, ou publier un message signé du nom de quelqu'un d'autre.
|
|
408
|
+
//
|
|
409
|
+
// Déplacer l'émission vers le serveur n'y changerait rien : sur un canal public, un
|
|
410
|
+
// attaquant émet quand même, et le client ne distingue pas les deux sources. La seule
|
|
411
|
+
// défense qui tienne est de CESSER DE CROIRE le transport — on relit auprès du serveur, qui
|
|
412
|
+
// est déjà la source de vérité (routes state=1 et chat=1, elles existaient).
|
|
413
|
+
//
|
|
414
|
+
// Un attaquant peut donc toujours émettre : il déclenche une relecture, et n'obtient rien.
|
|
415
|
+
// C'est une meilleure propriété que d'essayer de l'empêcher — elle vaut aussi le jour où le
|
|
416
|
+
// transport lui-même a un défaut.
|
|
417
|
+
//
|
|
418
|
+
// ⚠️ 'map' et 'typing' restent appliqués tels quels, et c'est un choix : ce sont des signaux
|
|
419
|
+
// ÉPHÉMÈRES (mouvements de carte, « untel écrit »), sans état serveur à confronter et à
|
|
420
|
+
// fréquence élevée. Les revérifier coûterait un aller-retour par déplacement de souris pour
|
|
421
|
+
// protéger… un déplacement de souris. Ce qui fait autorité — la page affichée, le document,
|
|
422
|
+
// la fin de la présentation — passe par 'state', qui est relu.
|
|
423
|
+
function relire(url,applique){
|
|
424
|
+
fetch('/api/doc?present='+encodeURIComponent(SLUG)+url)
|
|
425
|
+
.then(function(r){return r.json();})
|
|
426
|
+
.then(function(d){if(d&&d.ok)applique(d);})
|
|
427
|
+
.catch(function(){});
|
|
428
|
+
}
|
|
429
|
+
var _relEtat=null,_relChat=null;
|
|
430
|
+
// Groupé : dix diffusions d'affilée ne doivent pas produire dix requêtes.
|
|
431
|
+
function relireEtat(){clearTimeout(_relEtat);_relEtat=setTimeout(function(){
|
|
432
|
+
relire('&state=1',function(d){if(d.state&&_onState)_onState(d.state);});
|
|
433
|
+
},120);}
|
|
434
|
+
function relireChat(){clearTimeout(_relChat);_relChat=setTimeout(function(){
|
|
435
|
+
relire('&chat=1',function(d){
|
|
436
|
+
if(d.messages)d.messages.forEach(function(m){if(!addMsg(m))updateMsg(m);});
|
|
437
|
+
if(typeof d.locked!=='undefined')applyLock(d.locked);
|
|
438
|
+
});
|
|
439
|
+
},120);}
|
|
440
|
+
|
|
441
|
+
// Le message reçu sert à savoir QU'IL SE PASSE quelque chose, et à notifier ; son CONTENU
|
|
442
|
+
// vient de la relecture. Sans ça, une notification pourrait afficher un texte forgé.
|
|
443
|
+
ch.on('broadcast',{event:'msg'},function(){relireChat();});
|
|
444
|
+
ch.on('broadcast',{event:'msg-upd'},function(){relireChat();});
|
|
445
|
+
ch.on('broadcast',{event:'lock'},function(){relireChat();});
|
|
446
|
+
ch.on('broadcast',{event:'state'},function(){relireEtat();});
|
|
404
447
|
ch.on('broadcast',{event:'typing'},function(p){onTyping(p&&p.payload);});
|
|
405
|
-
ch.on('broadcast',{event:'lock'},function(p){if(p&&p.payload)applyLock(p.payload.locked);});
|
|
406
448
|
ch.on('broadcast',{event:'map'},function(p){if(_onMap&&p&&p.payload)_onMap(p.payload);});
|
|
407
|
-
ch.on('broadcast',{event:'state'},function(p){if(_onState&&p&&p.payload)_onState(p.payload);});
|
|
408
449
|
ch.subscribe(function(st){if(st==='SUBSCRIBED'){ch.track({name:me.name,email:me.email,avatar:me.avatar,role:me.role,member:!!me.member,uid:attKey(me)});sendAttend();}});
|
|
409
450
|
_tyIv=setInterval(renderTyping,1500);
|
|
410
451
|
_atIv=setInterval(sendAttend,25000);
|