Quel avenir pour les scripts javascript inline dans SPIP ?

Bonjour,

Les scripts JavaScript inline comprennent les <script>...</script> et également les onclick="...", onload="...", etc.

En revanche, ils ne comprennent pas les <script src="..."></script>.

Les bonnes pratiques d’une part, et les CSP (Content Security Policy) d’autre part, semblent pousser à l’abandon des scripts JavaScript inline.

Quelle est la politique de SPIP sur ce sujet ?

Cordialement,
Equipement

1 « J'aime »

Dans SPIP 5, une bonne proportion des événements appelés via l’attribut onclick ont (ou vont) basculer au profit d’écouteurs/déclencheurs utilisant des attributs data-.

Voir les bouton_action pour l’exemple.

Le chantier continue cependant.

À titre personnel, j’apprécie le recours des balises <script type="module"> au sein d’inclures spécialisés. Cette une méthode qui se marie plutôt bien avec l’écriture de squelettes et qui a le bon goût de résoudre pas mal d’inconvénients (import non doublonné avec chargement différé et asynchrone par défaut, donc plus de problème de concurrence de chargements des ressources et exit aussi la collision de nommage des variables si un inclure est injecté plusieurs fois.
Mais pour l’usage des modules ESM, il faut là aussi attendre. ( L’annuaire importmap est disponible dans la branche 5)

1 « J'aime »

Pour le chargement de modules, quels sont les inconvénients d’utiliser des balises <script type="module" src="..."></script>, par rapport aux balises <script type="module">...</script> (qui sont des scripts inline) ?

Il n’y a pas d’injonction fondamentale à utiliser l’un plutôt que l’autre.

Avec <script type="module" src="..."></script>, le module contient déjà tout ce dont il a besoin et s’éxécute de lui-même sur un markup du document déjà attendu ou alors en fonction d’un jeu de données accessible globalement via l’objet window.

Avec l’autre approche, on peut initialiser le code depuis le composant lui-même, avec des paramètres issus de son contexte (typiquement avec #ENV, ou encore #CONST ,#SELF, #PARAM, …); c’est particulièrement intéressant pour un composant appelé une multitude fois sur la page mais avec un contexte possiblement changeant (ex. : un item card animé, un slider, un picker, …), sur lequel on applique une fonctionnalité issue d’une lib tierce.

Aussi, on comprend en un clin d’œil ce qui se passe, (confort de dev), puisque le markup du composant et le code d’initialisation demeurent côte à côte.

<div id="[id(#ENV*|md5{substr{0,8}|set{uid,1})]">
//...
</div>
<script type="module">
  import {anim} from "script.js"
  const id = document.getElementById('[id(#GET{uid})]')
  anim(id,'[(#ENV{type,normal})]')
</script>

Cette approche était déjà possible auparavant avec les balises inline historiques <script>...</<script> ; seulement les tests conditionnels pour vérifier le chargement de l’objet jQuery (en fonction de la position de la page) + la résolution jamais doublonnée de la ressource via jQuery.getScript() rendait la mis en partique très fastidieuse.

Pour aller splus loin, je recommande la lecture de cette documentation (en français)

3 « J'aime »

Le code HTML peut exposer les données via les attributs data-* (datasets en HTML5), voire des balises « template » (en HTML5) pour des données volumineuses.

<div id="monElement" data-user='{"nom": "Alice", "age": 30, "actif": true}'>
  Profil utilisateur
</div>

Le fichier javascript <script type="module" src="..."></script> peut ensuite les utiliser.

// Sélectionner l'élément HTML
const element = document.getElementById('monElement');

// Lire l'attribut data-user et le convertir de JSON en objet JavaScript
const userData = JSON.parse(element.dataset.user);

// Utiliser les données de l'objet
console.log(userData.nom); // Affiche : Alice
console.log(userData.age); // Affiche : 30

Cela sépare les données et les traitements et évite que les CSP (Content Security Policy) désactivent le script inline <script type="module">.

1 « J'aime »

Hello,

I am also working on adding CSP to a large SPIP project of mine.

It seems still to be a very custom per-project process to have good CSP. When I check major frameworks and CMSes I find CSP is still avant-guarde and very custom per-project. But the earlier it’s implemented the better, it prevents by itself bad practices of inline scripts etc. So a default configuration would be a very good thing for new projects. But with existing projects, everything has to be checked before applying the header… And it will need to be adapted for the specific project.

To add the CSP, I decided to do it via a SPIP pipeline (to be added at Apache and Nginx level, but I want it to work out of the box).

If you have no inline scripts and styles, you can enforce a very strong CSP. But if you have inline styles and scripts, you can loosen the CSP to accommodate that, and use a nonce for the inline scripts and styles tags.

Over the years, in my project a lot of inline scripts have accumulated… For these we can use a nonce. The only issue there is that ajax forms will not match the original page’s nonce, so the nonce can’t be a true nonce, so for me, until these ajax-inline-scripts are removed, I will keep the nonce per session. It’s less secure, but already a huge win compared to no CSP, the attacked would need to guess the session’s nonce.

Create a nonce: (in mes_fonctions.php)
(for me it’s per session, but if you don’t have inline scripts in ajax forms, you can have a true per request-nonce):


// CSP nonce helper. Stored in session so it remains constant for the user's session
// Todo: use spip's session
function csp_nonce() {
	// ensure session is active
	if (session_status() === PHP_SESSION_NONE) { @session_start(); }

	if (!isset($_SESSION['csp_nonce'])) {
		$_SESSION['csp_nonce'] = base64_encode(random_bytes(16));
	}
	return $_SESSION['csp_nonce'];
}

Then, in all inline scripts, ensure nonce is there (throughout the squelettes):

<script nonce="' . csp_nonce() . '">

And finally, add a CSP header via mon_plugin_pipelines.php (or instead via Apache/Nginx config):

/**
 * Add security headers (CSP + related headers) to every public page.
 *
 * This pipeline sets a conservative Content-Security-Policy and a per-request
 * nonce which can be injected into inline <script nonce="..."> tags when
 * rendering trusted inline scripts.
 *
 * @pipeline affichage_entetes_final
 * @param array $entetes Associative array of headers
 * @return array Modified headers
 */
function mon_plugin_affichage_entetes_final($entetes) {
    // Only add CSP for HTML responses. The pipeline receives an associative
    // array of headers; check for content-type. If none present, assume HTML
    // for public pages and proceed.

    // is added to any response that goes through spip (careful, svgs and pdfs are serverd by apache directly. )


    $nonce = biz_csp_nonce();

    // Compose CSP; prefer a conservative default. This may be made configurable.
    $parts = [];
    $parts[] = "default-src 'self'";
    // Allow our analytics host explicitly so matomo.js and tracking beacons are allowed
    $parts[] = "script-src 'self' 'nonce-{$nonce}' https://stats.example.com https:";

    // Allow inline styles ('unsafe-inline') unconditionally per your request.
    // Note: this weakens CSP but permits style="..." usage site-wide.
    $style_src = "'self' 'nonce-{$nonce}' 'unsafe-inline' https:";
    $parts[] = "style-src {$style_src}";

    // Some browsers enforce separate style-src-attr/style-src-elem directives.
    // Explicitly allow inline style attributes and ensure style elements are covered.
    $parts[] = "style-src-attr 'unsafe-inline'";
    $parts[] = "style-src-elem 'self' 'nonce-{$nonce}' https:";

    // Images (allow analytics pixel if needed)
    $parts[] = "img-src 'self' data: https://stats.example.com https:";
    // Allow fonts from same origin, https and data: URIs (some components inline-fonts via data:)
    $parts[] = "font-src 'self' data: https:";
    $parts[] = "frame-src 'self' https://www.youtube.com https://player.vimeo.com";
    // Allow connect to analytics (beacons, XHRs)
    $parts[] = "connect-src 'self' https://stats.example.com https:";
    $parts[] = "object-src 'none'";
    $parts[] = "base-uri 'self'";

    $csp = implode('; ', $parts);

    // Allow worker scripts created from blob: URLs (used by some libraries)
    $csp .= "; worker-src 'self' blob: https:";

    // Add a reporting endpoint so browsers can POST violation reports.
    // The endpoint includes the current page as a "context" parameter so the
    // receiver can attribute reports to the originating page.
    $report_url = url_absolue('/spip.php?page=api_csp&context=' . rawurlencode(self()));
    $csp .= '; report-uri ' . $report_url;

    // Use enforcement by default; switch to Report-Only via configuration if desired
    $entetes['Content-Security-Policy'] = $csp;

    // Complementary security headers
    $entetes['X-Content-Type-Options'] = 'nosniff';
    $entetes['X-Frame-Options'] = 'SAMEORIGIN';
    // For Matomo we want to send the full URL as Referer. This sets a permissive
    // policy so the browser will include the full URL for cross-origin requests.
    // WARNING: this exposes path/query information to third-party hosts.
    $entetes['Referrer-Policy'] = 'unsafe-url';

    // Enforce HSTS fully when the request is served over HTTPS. Include SubDomains
    // and request preload. Detect HTTPS via common server variables to work behind
    // reverse proxies (X-Forwarded-Proto).
    $is_https = (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off');
    if ($is_https) {
        // 1 year max-age; includeSubDomains and preload for full enforcement
        $entetes['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains; preload';
    }

    return $entetes;
}

It’s a work in progress for me, but I thought I’d share my approach.

In future never having any inline scripts and styles is the solution, and then there is no nonce, and the CSP header can be very strong…

Urs

1 « J'aime »

If you want another option, you can write the nonce in apache directly.
LoadModule cspnonce_module /opt/homebrew/lib/httpd/modules/mod_cspnonce.so

1 « J'aime »

thx @ursrig for the tip about using nonce attibute on inline tags for CSP concerns,

A few remarks thought .

Instead of using user session to keep alive the random csp_nonce key, i’d rather re-generated it from scratch on every php hit and insert it wherever it is needed as it should not be cached anywhere once the page has been fully delivered (no need to make it persistant in session).

You are not compelled to insert literal php code here <script nonce="' . csp_nonce() . '"> ; you could define a #CSP_NONCE balise using calculer_balise_dynamique() that is made for this purpose.

Moreover, as far as <script/> with src attribute is concerned, it might be a better option to insert integrity attribute with a hashed signature as it will be served as a static content, especially after the compresseur job is done via produire_fond_statique()

All theses features should be discussed and integrated in the upcoming spip/frontend plugin !

1 « J'aime »

Concernant l’attribut « nonce », l’auteur du plugin spip_csp a écrit :

Insertion (de nonce) via le pipeline affichage_final (SPIP) :

Modification de toutes les balises qui intègrent du code ‹ inline › pour leur ajouter l’attribut ‹ nonce ›. Cette technique, en cours de standardisation dans CSP 1.1 permet ainsi d’exécuter les scripts ‹ inline › jugés valides côtés serveur.
Remarques :
- le pipeline est n’est ici pas idéal, il prend toutes les balises scripts, même celles qui aurait pu être injectées via une XSS permanente (elle ne permet par exemple d’empêcher l’exploitation de la vulnérabilité corrigée dans SPIP 3.0.14.
- on utilise ici le pipeline ‹ affichage_final ›, ce qui n’est clairement pas optimal (pas de mise en cache).

Source : spip_csp/csp_pipelines.php at df6af6ac8f8babd3970fd1daf46532d71fd4ab76 · otetard/spip_csp · GitHub

Par ailleurs, avec cette technique de ‹ nonce ›, les « onclick=‹ … › » ou similaires resteraient désactivés par une CSP interdisant les scripts inline.

Remarque : Il existe la technique des ‹ unsafe-hashes ›, qui permet d’autoriser uniquement les handlers inline dont le contenu correspond à un hash explicitement déclaré. Par exemple : Content-Security-Policy: script-src ‹ self › ‹ unsafe-hashes › ‹ sha256-… › . Toutefois, avec un très grand nombre de « onclick=‹ … › » ou similaires, il faudrait un très grand nombre de hash dans la CSP. En plus, en cas d’évolution d’un de ces codes javascript, il serait nécessaire de mettre à jour la CSP.

Les sha256-xxx dans la CSP permettent de gérer les plugins qui injectent du JS inline statique, même s’il y en a plusieurs. Le risque est effectivement d’avoir des JS qui évoluent.

« même s’il y en a plusieurs » … dont le code correspond à un hash autorisé.

unsafe-hashes : Autorise les scripts inline si leur hash correspond à un hash autorisé.

1 « J'aime »

My policy is the following:

For new projects, and this could become a kind of default in squelettes-dist and SPIP, is to not have any style=«  » and onclick=«  » calls at all.
No inline and tags either.

What’s nice is if this is enforced from the start, it serves as a reminder to never inline anything. Done!

For existing projects I am either converting to the above.
But i have one project with a lot of inline scripts and I definitely did not want to use affichage_final because that would defeat the purpose of eliminating XSS as @equipement quoted from spip_csp’s author.

I am adding to all the scripts a #NONCE or <?php echo csp_nonce();?>, and requiring nonce.

And to get rid of session-based nonces for ajax forms, I one should simply remove any inline scripts in forms, period.

–

I am thinking that CSP default enforcement via squelettes-dist or a configuration form could be a very interesting development for SPIP, or as part of a plugin, or to review the csp plugin that was available in SPIP 3.

Here the question would be:
Is CSP something that can be made configurable, easy to understand and apply for non-technical users? It would add a lot of security, but for now, a lot of testing and customization is needed in any case. But a simple « Activate strict CSP » and a box wiht allowlisted domains like youtube, would probably work very well! Which may be exactly what the spip3 plugin provides…

Bonjour @placido ,

Existe-t-il une présentation / fil de discussion au sein de SPIP à ce propos ?
Un certain nombre de plugins utilisent encore des onclick(), ce qui n’arrange pas nos soucis de gestion des CSP.

1 « J'aime »

Il n’y a pas de ticket dédié à l’implémentation de CSP.
En revanche l’éviction des attributs onXXX est un corollaire pratique du chantier Déprécier l’usage de jQuery, migrer tranquillement en vanilla JS (#5566) · Issues · spip / spip · GitLab.

Si des personnes intéressées et motivées veulent s’y pencher, il faut ouvrir un ticket ici. (Le dépot est dans une phase balbutiante, il faut que je trouve le temps de reformuler une MR qui y transfère un certain nombre de features depuis spip/prive et spip/ecrire ; j’avais des branches fonctionnelles en local mais qui datent de plusieurs mois maintenant)