Skip to content

Échapper la sortie HTML (à l'épreuve du XSS) snippet

Échappe le texte non fiable avant qu'il n'atterrisse en HTML — la ligne de défense contre le XSS.

Échappe le texte non fiable avant qu'il n'atterrisse en HTML — la ligne de défense contre le XSS. La règle encore largement enfreinte : échapper à la SORTIE (à la frontière du template), jamais sanitizer puis stocker l'entrée, car le contexte d'échappement (corps HTML contre attribut contre chaîne JS contre URL) n'est connu qu'au moment du rendu. Et une minuscule allowlist bat toujours une blacklist : encode & < > " ' et rien d'autre — retirer les balises script par nom est un jeu de taupes qui perd contre <scr<script>ipt.

Recette exécutable · 12 langagesOpen the html-entity-encoder tool →
Frontend & DOMxsshtmlescapesecurityencodingfrontend

Every language

12 langages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
function escapeHtml(s) {
  return s.replace(/[&<>"']/g, (c) => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
  }[c]));
}

// safe in element text AND in attribute values — the five cover both:
div.innerHTML = `<option value="${escapeHtml(city)}">${escapeHtml(city)}</option>`;

Escape in the RIGHT context — attribute values need " and ' escaped too, and the five-character map covers body text and attributes. But a URL in href needs percent-encoding, not entity-encoding. innerHTML with escaped text is fine; innerHTML with unescaped text never is.

TSTypeScript
const ENTITIES: Record<string, string> = {
  '&': '&amp;',
  '<': '&lt;',
  '>': '&gt;',
  '"': '&quot;',
  "'": '&#39;',
};

export function escapeHtml(s: string): string {
  return s.replace(/[&<>"']/g, (c) => ENTITIES[c]);
}

One map pass beats chained .replace() calls — when hand-chaining, the ORDER is load-bearing: & must be replaced FIRST, or the '&lt;' emitted by the < pass gets re-escaped into '&amp;lt;'. Record<string, string> keeps the lookup typed with no index-signature gymnastics.

GoGo
import "html"

// HTML text contexts — escapes & < > ' " :
func renderComment(body string) string {
	return "<p>" + html.EscapeString(body) + "</p>"
}

html.EscapeString is for HTML TEXT, NOT URLs — url.PathEscape / url.QueryEscape handle href and query sinks, and a <script> block needs JS-string escaping, not entities. Better still: html/template escapes interpolations automatically; reaching for text/template is the classic Go XSS bug.

RsRust
use html_escape::encode_text;

fn render_comment(body: &str) -> String {
    format!("<p>{}</p>", encode_text(body)) // & < > " ' — text context
}

Askama and handlebars-rust auto-escape {{ }} interpolations in HTML templates — the XSS bug is format!()-ing untrusted text into raw HTML strings, or opting out with the `safe` filter. Outside a template, the html_escape crate's encode_text is the maintained escaper.

PHPPHP
function e(string $s): string {
    return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

// the one-letter helper every template defines:
<?= e($userComment) ?>

ENT_QUOTES is mandatory — without it " and ' pass through raw and a single-quoted attribute pops open. ENT_SUBSTITUTE turns invalid UTF-8 into U+FFFD instead of an empty string. strip_tags is NOT escaping: it deletes complete tags and keeps every other dangerous byte.

PyPython
import html

def render_comment(body: str) -> str:
    return f"<p>{html.escape(body, quote=True)}</p>"  # & < > " '

quote=True (the default) is what makes it attribute-safe — " and ' are encoded too; quote=False leaves them raw. In Flask/Jinja, MarkupSafe integrates with autoescape, and Markup() opts OUT of it exactly like Rails' raw() — audit those call sites.

C#C#
using System.Net;
using System.Text.Encodings.Web;

string body = WebUtility.HtmlEncode(input);       // & < > " only
string attr = HtmlEncoder.Default.Encode(input);  // all five — attribute-safe

WebUtility.HtmlEncode does NOT encode the single quote — fine for element text, a hole inside single-quoted attributes. HtmlEncoder.Default (System.Text.Encodings.Web, successor to the retired Microsoft.AntiXSS) covers all five for attribute-safe output.

JvJava
import org.owasp.encoder.Encode;

String body = Encode.forHtml(comment);          // & < > " '
String attr = Encode.forHtmlAttribute(comment); // the context-typed spelling

Spring's HtmlUtils.htmlEscape is the no-extra-dependency option for HTML body text; OWASP Encoder gives per-sink spellings (forHtml, forHtmlAttribute, forJavaScript). commons-lang StringEscapeUtils targets Java/JSON string contexts — it is not an HTML escaper.

SwSwift
import Foundation

func escapeHtml(_ s: String) -> String {
    s.replacingOccurrences(of: "&", with: "&amp;")    // & FIRST — or every
     .replacingOccurrences(of: "<", with: "&lt;")     // entity you just
     .replacingOccurrences(of: ">", with: "&gt;")     // wrote gets
     .replacingOccurrences(of: "\"", with: "&quot;")  // re-escaped
     .replacingOccurrences(of: "'", with: "&#39;")
}

Swift ships no stdlib HTML escaper — the mapped-replace hand-roll is the answer, and its ORDER is load-bearing: & first, or '&amp;' becomes '&amp;amp;'. NSAttributedString is a markup parser, not an escaper — do not reach for it here.

KtKotlin
import org.owasp.encoder.Encode

fun String.escapeHtml(): String = Encode.forHtml(this)

val safe = comment.escapeHtml() // same JVM escapers as Java, one line shorter

Same JVM options as Java (OWASP Encode, Spring HtmlUtils) behind a one-line extension — and the same trap: pick the escaper for the SINK (forJavaScript for <script> blocks), never one function for every context.

RbRuby
require 'erb'

ERB::Util.html_escape(comment)  # & < > " '
CGI.escapeHTML(comment)         # same five — stdlib, no require in Rails

Rails and Erubi escape <%= %> by default — the leaks are raw() and .html_safe opting OUT; audit every one of those call sites. ERB::Util.html_escape and CGI.escapeHTML encode the same five characters.

ZigZig
const std = @import("std");

fn escapeHtml(writer: anytype, s: []const u8) !void {
    for (s) |c| switch (c) {
        '&' => try writer.writeAll("&amp;"),
        '<' => try writer.writeAll("&lt;"),
        '>' => try writer.writeAll("&gt;"),
        '"' => try writer.writeAll("&quot;"),
        '\'' => try writer.writeAll("&#39;"),
        else => try writer.writeByte(c),
    };
}

No std library ships HTML escaping — hand-roll it into a writer (std.Io.Writer or an ArrayList(u8) you own). The single-pass switch makes the & -first rule structural: emitted entities are never re-read. If you instead run successive mem.replace passes over a buffer, & MUST go first or &amp; becomes &amp;amp;. UTF-8 bytes >= 0x80 hit `else` and copy through untouched.

Keep going

Try the interactive html-entity-encoder tool →