8697 sujets

Développement web côté serveur, CMS

Bonsoir,

Je me retrouve confronté à un problème d'autorisation CSP pour YouTube :
Refused to connect to 'https://youtube.com/oembed?url=http://youtube.com/watch?v=3Bs4LOtIuxg' because it violates the following Content Security Policy directive: "default-src 'self' 'unsafe-inline'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

Ma stack est basée sur Fastify et j'utilise Helmet pour gérer les autorisation CSP, plus exactement une version encapsulé pour Fastify : @fastify/helmet. Fondamentalement sous le capot c'est la même chose, cependant je précise au cas où...

Voici ma config' sous Fastify :
import helmet from '@fastify/helmet'

app.register(helmet, {
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self' 'unsafe-inline'"],
      imgSrc: [
        "'self' data: *.openstreetmap.org",
        "'self' data: *.openstreetmap.fr",
        "'self' data: *.ign.fr",
        "'self' data: *.youtube.com",
        "'self' data: *.ytimg.com", // YouTube, certaines miniatures du player et notamment l'image par défaut.
      ],
      frameSrc: ["'self' data: *.youtube.com"],
    },
  },
})

Il est certain que le problème ne vient pas d'ailleurs que ce code (ex: configuration serveur) car lorsque je supprime le code Helmet ici présent je retrouve l'usage de mes lecteurs YouTube. À l'inverse, les autorisations pour OpenStreetMap et IGN ne posent aucun problème.

Ce qui me rend fou c'est que lorsque je prototypais sous Express.js je n'avais aucun problème de lecture de mes vidéos YouTube :
const helmet = require('helmet')

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self' 'unsafe-inline'"],
      imgSrc: [
        "'self' data: *.openstreetmap.org",
        "'self' data: *.openstreetmap.fr",
        "'self' data: *.ign.fr",
        "'self' data: *.youtube.com",
        "'self' data: *.ytimg.com", // YouTube, certaines miniatures du player et notamment l'image par défaut.
      ],
      frameSrc: ["'self' data: *.youtube.com"],
    },
  })
)

Voilà voilà, si quelqu'un d'entre a une idée, à moins que le problème soit là sous mes yeux... dans tous les cas je suis preneur.
Modifié par Olivier C (09 Feb 2024 - 22:38)
Modérateur
Salut,

peut être une une petite piste : regarde dans l'onglet réseau les urls appelées.

Il me semble que tu puisses régler le problème des csp avec une <meta />
niuxe a écrit :
Peut être une une petite piste : regarde dans l'onglet réseau les urls appelées.

Lorsque je supprime les règles Helmet les ressources YouTube sont bien présentes avec un statut HTTP de 200. Sinon nada, tout est bloqué et elles n'apparaissent même pas.
Salut,
normalement, les règles CSP pour les medias se font avec media-src. Mais je n'ai jamais utilisé helmet pour mettre en place le CSP, c'est sans doute différent. Je mets le CSP directement dans le htaccess, en texte.
Parce que tes url youtube et autres sont dans img-src. D'ailleurs pour frame, tu as sorti la règle de img-src pour la mettre dans fram-src.
Modifié par Bongota (09 Feb 2024 - 23:23)
Maintenant, si tu veux quitter helmet et le faire à la main, voici, je suis généreux à 23 h et quelques :
# Content-Security-Policy
<IfModule mod_headers.c>
    Header set Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self'; base-uri 'self'; font-src 'self'; object-src 'none'; media-src 'self'  https://mesvideos.b-cdn.net;"
 
</IfModule>
# /Content-Security-Policy

Modifié par Bongota (09 Feb 2024 - 23:26)
Bongota a écrit :
Parce que tes url youtube et autres sont dans img-src.

Pour les images oui, encore que, comme je l'ai commenté dans mon code, il faut aussi "*.ytimg.com" si l'on veut être sûr de récupérer toutes les images miniatures. Mais pour la vidéo par elle-même il faut bien "frame-src" en plus.
Bongota a écrit :
D'ailleurs pour frame, tu as sorti la règle de img-src pour la mettre dans fram-src.

Dans mon code le nom de domaine YouTube est placé au deux endroits.
Modifié par Olivier C (09 Feb 2024 - 23:34)
Bon, au final j'ai trouvé une solution qui ne me satifait pas car j'ai mis un paramètre "https:" dans "defaut-src", ce qui n'est pas top à mon sens, et aussi ça ne m'explique pas pourquoi cela fonctionnait sur Express.

Solution temporaire avec Helmet (nettoyé de ses redondances au passage) :
import helmet from '@fastify/helmet'

app.register(helmet, {
  contentSecurityPolicy: {
    //useDefaults: false,
    directives: {
      defaultSrc: ["'self'  https:"], 
      imgSrc: [
        "'self'",
        'data:',
        '*.openstreetmap.org',
        '*.openstreetmap.fr',
        '*.ign.fr',
        '*.youtube.com',
        '*.ytimg.com', // YouTube, certaines miniatures du player et notamment l'image par défaut.
      ],
      frameSrc: ["*.youtube.com"],
    },
  },
  //crossOriginEmbedderPolicy: false,
  //crossOriginOpenerPolicy: true
})

La même chose sans Helmet, placé dans un "hook" Fastify (la solution que je vais retenir au final, un poil plus rapide, mais surtout moins "magique" pour moi) :
app.addHook('onRequest', (req, res, done) => {
  // prettier-ignore
  res.header(
    'Content-Security-Policy', "default-src 'self'  https:;base-uri  'self';font-src 'self'  https:  data:;form-action 'self';frame-src 'self' data: *.youtube.com;img-src 'self' data: *.openstreetmap.org *.openstreetmap.fr *.ign.fr *.youtube.com *.ytimg.com;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self'  https:  'unsafe-inline';upgrade-insecure-requests",
    'Cross-Origin-Opener-Policy', 'same-origin',
    'Cross-Origin-Resource-Policy', 'same-origin',
    'Origin-Agent-Cluster', '?1',
    'Referrer-Policy', 'no-referrer',
    'Strict-Transport-Security', 'max-age=15552000; includeSubDomains',
    'X-Content-Type-Options', 'nosniff',
    'X-DNS-Prefetch-Control', 'off',
    'X-Download-Options', 'noopen',
    'X-Frame-Options', 'SAMEORIGIN',
    'X-Permitted-Cross-Domain-Policies', 'none',
    'X-XSS-Protection', '0',
  ) // Set referrer policy to same-origin
  done()
})

Pour ceux que cela intéresse j'ai développé longuement sur StackOverflow.

Dans mon malheur j'ai appris plein de trucs que je ne connaissais pas, mais ceci dit cela ne me donne pas encore le mot de la fin, mon ticket reste ouvert...
Bongota a écrit :
Maintenant, si tu veux quitter helmet et le faire à la main, voici, je suis généreux à 23 h et quelques :
# Content-Security-Policy
&lt;IfModule mod_headers.c&gt;
    Header set Content-Security-Policy "style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self'; base-uri 'self'; font-src 'self'; object-src 'none'; media-src 'self'  https://mesvideos.b-cdn.net;"
 
&lt;/IfModule&gt;
# /Content-Security-Policy

J'ai passé en revue ton code Bongota : le paramètre `media-src` n'existe pas dans la spécification et soulève une erreur. Tu as aussi une absence de `default-src`, du coup tout fonctionne globalement, mais c'est moins sécurisé.

En même temps, de mon côté, avec mon patch `default-src 'self' https:` je viens de faire un énorme trou dans mon CSP... Smiley confus
Salut,
étrange, je tente une validation de mon code sur https://csp-evaluator.withgoogle.com/

upload/1707642620-67790-csp-validation.png

Et sur le site https://developer.mozilla.org/fr/docs/Web/HTTP/CSP
dans l'exemple n° 3, la directive media-src est présente.
Au sujet de défault-src, il est dit, sur https://developer.mozilla.org/fr/docs/Glossary/Fetch_directive

"Toutes les directives de récupération reviennent à default-src. Cela signifie que si une instruction fetch est absente dans l'en-tête CSP, l'agent utilisateur recherchera la directive default-src."
Je peux effectivement ajouter default-src.
Et avec unsafe-inline tu as ouvert aussi une autre porte, comme chez moi. Mais il est difficile de l'éviter celle-ci.
Personne ne t'a encore répondu sur stackOverflow.
Modifié par Bongota (11 Feb 2024 - 10:12)
@Bongota : Autant pour moi pour `media-src`, j'ai mal interprêté une erreur en console (moi et l'anglais ça fait deux) et je n'étais pas allé jusqu'à vérifier l'existence de cette règle sur MDN. Effectivement elle existe bel et bien et concerne les balises HTMLMediaElement.

Et oui : personne n'a répondu sur StackOverflow Smiley decu (mais il faut dire que c'est assez élitiste comme système, même si c'est bien pratique).

Quoi qu'il en soit j'ai trouvé une solution satisfaisante que je posterais plus tard, quand je l'aurai éprouvé un peu. Et les infos sympas et bien présentées, je les ai trouvées sur... Alsacreations : Guidelines, Sécurité front-end (HTTP).

Je renonce à savoir pourquoi avec la même config Helmet je n'avais pas la même résultat sur Express que sur Fastify. C'était un mauvais point de départ pour débuger et j'ai perdu pas mal de temps avec ça. Mais au final ce n'est pas très important, il vaut mieux se concentrer sur la logique CSP, c'est bien plus formateur et profitable. Du coup, suppression de Helmet.js : moins de "magie" et plus de compréhension du code fondamental (et en plus c'est plus rapide de 5 à 10% par rapport à Helmet selon les dire même du créateur du plugin). C'est toujours ça de pris.
Modifié par Olivier C (11 Feb 2024 - 14:44)
Bon, et bien au final j'ai écrit ma propre solution de remplacement pour Helmet.js et ce faisant, non seulement je me suis familiarisé avec les règles CSP (que je n'avais jamais pris le temps de connaître), mais j'ai écrit de zéro mon premier plugins Fastify non encapsulé :
import fastifyPlugin from 'fastify-plugin'

/**
 * Configuration des en-têtes HTTP
 * @note Utilisation d'un Hook pour paramétrer le header au détriment de la solution clef en main `@fastify/helmet`, ceci afin d'éviter une solution "boite noire" qui fonctionnera de manière "magique" pour de developpeur.
 * @see  https://helmetjs.github.io/faq/you-might-not-need-helmet/
 
 * @param {FastifyInstance} app
 */
async function HTTPHeaders(app) {
  await app.addHook('onRequest', (req, res, done) => {
    res.headers({
      'Content-Security-Policy': [
        "default-src 'self' 'unsafe-inline' data:  https:", 
        "base-uri 'self'",
        "font-src 'self'",
        "form-action 'self'",
        "frame-src 'self' *.youtube.com",
        "media-src 'self'",
        "img-src 'self' data:  https:", 
        "object-src 'none'",
        "script-src 'self'",
        "script-src-attr 'none'",
        "style-src 'self' 'unsafe-inline'",
        "manifest-src 'self'",
        'upgrade-insecure-requests',
      ],
      'Cross-Origin-Opener-Policy': 'same-origin',
      'Cross-Origin-Resource-Policy': 'same-origin',
      'Origin-Agent-Cluster': '?1',
      'Referrer-Policy': 'no-referrer',
      'Strict-Transport-Security': 'max-age=15552000; includeSubDomains',
      'X-Content-Type-Options': 'nosniff',
      'X-DNS-Prefetch-Control': 'off',
      'X-Download-Options': 'noopen',
      'X-Frame-Options': 'SAMEORIGIN',
      'X-Permitted-Cross-Domain-Policies': 'none',
      'X-XSS-Protection': '0',
    }) // Set referrer policy to same-origin
    done()
  })
}

export default fastifyPlugin(HTTPHeaders)

On en apprend tous les jours...

J'ai passé les règles CSP en tableau pour que cela soit plus simple à lire mais c'est tout à fait valide côté en-têtes HTTP. Faisons un coup de `curl -I` pour vérifier :
$ curl -I  http://127.0.0.1:7001/
 
HTTP/1.1 200 OK
content-security-policy: default-src 'self' 'unsafe-inline' data:  https:
 
content-security-policy: base-uri 'self'
content-security-policy: font-src 'self'
content-security-policy: form-action 'self'
content-security-policy: frame-src 'self' *.youtube.com;media-src 'self'
content-security-policy: img-src 'self' data:  https:
 
content-security-policy: object-src 'none'
content-security-policy: script-src 'self'
content-security-policy: script-src-attr 'none'
content-security-policy: style-src 'self' 'unsafe-inline'
content-security-policy: upgrade-insecure-requests
cross-origin-opener-policy: same-origin
cross-origin-resource-policy: same-origin
origin-agent-cluster: ?1
referrer-policy: no-referrer
strict-transport-security: max-age=15552000; includeSubDomains
x-content-type-options: nosniff
x-dns-prefetch-control: off
x-download-options: noopen
x-frame-options: SAMEORIGIN
x-permitted-cross-domain-policies: none
x-xss-protection: 0
content-type: text/html; charset=utf-8
content-length: 6391
Date: Sun, 11 Feb 2024 13:01:22 GMT
Connection: keep-alive
Keep-Alive: timeout=72

Sujet résolu.
Modifié par Olivier C (11 Feb 2024 - 19:51)
Meilleure solution