class Components::SafeStyle

Overview

SafeStyle is the typed style-attribute builder the hardening proposal §3.3 requires: "Ban raw strings; require a typed Style/color builder." This is what closes the CSS-context sink class the plain brand_color mailer bug (Stage 5, commit c6ec54f) fell into — HTML.escape alone would not have caught a CSS-context breakout, because the attack lives in a different parser (the CSS value parser), not the HTML parser.

Design: a narrow property allowlist, each property either typed (SafeColor/SafeLength) or checked against a denylist of dangerous CSS constructs (url(...), expression(...), @import, embedded javascript:, statement separators). "Ban until typed" is the default — adding a property means adding it to the allowlist deliberately, not opening a general string hole.

Defined in:

components/safe/safe_style.cr

Constant Summary

COLOR_PROPERTIES = ["color", "background-color", "border-color", "outline-color"] of ::String
KEYWORD_PROPERTIES = {"display" => ["none", "block", "inline", "inline-block", "flex", "inline-flex", "grid", "inline-grid", "contents"] of ::String, "position" => ["static", "relative", "absolute", "fixed", "sticky"] of ::String, "text-align" => ["left", "right", "center", "justify", "start", "end"] of ::String, "font-weight" => ["normal", "bold", "lighter", "bolder", "100", "200", "300", "400", "500", "600", "700", "800", "900"] of ::String, "flex-direction" => ["row", "row-reverse", "column", "column-reverse"] of ::String, "align-items" => ["stretch", "flex-start", "flex-end", "center", "baseline"] of ::String, "justify-content" => ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"] of ::String, "overflow" => ["visible", "hidden", "scroll", "auto", "clip"] of ::String, "overflow-x" => ["visible", "hidden", "scroll", "auto", "clip"] of ::String, "overflow-y" => ["visible", "hidden", "scroll", "auto", "clip"] of ::String, "white-space" => ["normal", "nowrap", "pre", "pre-line", "pre-wrap"] of ::String, "text-transform" => ["none", "capitalize", "uppercase", "lowercase"] of ::String}
LENGTH_PROPERTIES = ["width", "height", "min-width", "min-height", "max-width", "max-height", "padding", "padding-top", "padding-bottom", "padding-left", "padding-right", "margin", "margin-top", "margin-bottom", "margin-left", "margin-right", "gap", "border-radius", "font-size", "line-height"] of ::String

Constructors

Instance Method Summary

Constructor Detail

def self.new #

[View source]

Instance Method Detail

def build : SafeStyleValue #

[View source]
def color(property : String, value : Color) : self #

A color-bearing property. Only COLOR_PROPERTIES are accepted; the value must be a Color, never a string.


[View source]
def keyword(property : String, value : String) : self #

A keyword-bearing property (e.g. display: flex). The value must be one of the property's own allowlisted keywords — this is how KEYWORD_PROPERTIES stays a closed set rather than a string hole.


[View source]
def length(property : String, value : SafeLength) : self #

A length-bearing property. Only LENGTH_PROPERTIES are accepted; the value must be a SafeLength, never a string — so expression(...), url(...), and --custom-property: passthrough are unreachable here.


[View source]