Skip to content

Escapar salida HTML (a prueba de XSS) snippet

Escapa el texto no confiable antes de que llegue al HTML — la línea de defensa contra el XSS.

Escapa el texto no confiable antes de que llegue al HTML — la línea de defensa contra el XSS. La regla que todavía se rompe por todas partes: escapa en la SALIDA (en la frontera de la plantilla), nunca sanitices-y-guardes la entrada, porque el contexto de escape (cuerpo HTML vs atributo vs string de JS vs URL) solo se conoce en el momento de renderizar. Y una allowlist diminuta le gana a una blacklist siempre: codifica & < > " ' y nada más — eliminar etiquetas «script» por nombre es un juego de golpear al topo que pierde contra <scr<script>ipt.

Receta ejecutable · 12 lenguajesAbrir la herramienta html-entity-encoder →
Frontend & DOMxsshtmlescapesecurityencodingfrontend

Every language

12 lenguajes, 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 →