From 76833b9e684f0f05931adabf400bf61d32692e31 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Thu, 21 May 2026 23:32:14 -0500 Subject: [PATCH 1/4] Add some ui-lib tests and fix quote escapes --- t/ui-lib-escaping.t | 135 ++++++++++++++++++++++++++++++++++++++++++++ ui-lib.pl | 19 +++++-- 2 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 t/ui-lib-escaping.t diff --git a/t/ui-lib-escaping.t b/t/ui-lib-escaping.t new file mode 100644 index 000000000..ffc441244 --- /dev/null +++ b/t/ui-lib-escaping.t @@ -0,0 +1,135 @@ +#!/usr/bin/perl +# Tests for ui-lib.pl attribute escaping and XSS resistance. +# +# These cover the *default* (non-theme) code path. Each function checks for +# a theme override first and returns early if one is defined; when ui-lib.pl +# is loaded as a library, no theme is present, so the default path runs. +# +# The core invariant: caller-supplied data interpolated into HTML must not +# escape its attribute context. We check this by stripping all quoted +# attribute values from the output and asserting that no event-handler +# attribute (on*=) survives. If the input broke out of an attribute, the +# handler appears outside any quoted region and the assertion fails. + +use strict; +use warnings; +use Test::More; +use File::Basename qw(dirname); +use File::Spec; + +my $root = File::Spec->rel2abs(File::Spec->catfile(dirname(__FILE__), '..')); +require File::Spec->catfile($root, 'web-lib-funcs.pl'); +require File::Spec->catfile($root, 'ui-lib.pl'); + +# Strip all "..." and '...' quoted regions from $html so that what remains +# is tag scaffolding only. Any attribute introduced by an attribute-quote +# breakout will appear in the stripped output. +sub strip_attr_values { + my ($html) = @_; + $html =~ s/"[^"]*"//g; + $html =~ s/'[^']*'//g; + return $html; +} + +sub assert_no_handler_injection { + my ($html, $label) = @_; + my $bare = strip_attr_values($html); + unlike($bare, qr/\bon[a-z]+\s*=/i, + "$label: no event-handler attribute leaks out of attribute value"); +} + +# Attribute-breakout payloads. The first is for double-quoted attrs, the +# second for single-quoted; we use whichever matches what the function +# emits (or both, when uncertain). +my $xss_dq = q{x"onmouseover="alert(1)}; +my $xss_sq = q{x' onmouseover='alert(1)}; + +# ---- safe-contract regression tests --------------------------------------- +# These functions already escape correctly today. The tests lock that down. + +assert_no_handler_injection(main::ui_textbox('field', $xss_dq, 20), + 'ui_textbox value'); +assert_no_handler_injection(main::ui_textbox($xss_dq, 'safe', 20), + 'ui_textbox name'); + +# ui_textarea must escape < so a value cannot inject . +{ + my $payload = q{}; + my $html = main::ui_textarea('field', $payload, 5, 40); + unlike($html, qr{ $xss_dq }), + 'ui_tag_start data attribute'); + +# ---- the three XSS sites we are about to fix ------------------------------ + +# ui_help: title goes into aria-label="...". Single-quote breakout doesn't +# apply (attr is double-quoted), but double-quote breakout does. +assert_no_handler_injection(main::ui_help($xss_dq), + 'ui_help title'); + +# ui_img: src/alt/title all in single-quoted attrs today. +assert_no_handler_injection(main::ui_img('img.png', $xss_sq), + 'ui_img alt'); +assert_no_handler_injection(main::ui_img('img.png', 'safe', $xss_sq), + 'ui_img title'); +assert_no_handler_injection( + main::ui_img(q{img.png' onerror='alert(1)}, 'safe'), + 'ui_img src'); + +# js_redirect: url must not be able to close the script tag, and the +# timeout/window parameters must not allow JS injection. +{ + # A literal in script data closes the script element. + # An extra exits it. So we + # only assert on the close-tag count. + my $url = q{/foo}gi; + is($closes, 1, 'js_redirect: url cannot inject '); +} +{ + # }; + my $html = main::js_redirect($url); + my $closes = () = $html =~ m{}gi; + is($closes, 1, 'js_redirect: url cannot inject