#!/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{ 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