CWE-80: Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS) - Perl
Overview
CWE-80 is the tag-injection case of XSS: <, > and & reach the page without
being converted to entities, so an attacker's <script> or
<img onerror=...> is parsed as markup rather than shown as text.
The fix is the output encoding described on
CWE-79 - Perl, which is where the encoders live:
CGI.pm's escapeHTML(), HTML::Entities::encode_entities(), URI::Escape for
URL context, JSON encoding for script context, and the Template Toolkit and
Mason escaping settings. This page does not repeat them.
What belongs here is the failure mode that produces CWE-80 findings specifically - trying to remove the dangerous tags instead of encoding the characters that make them tags.
Common Vulnerable Patterns
Stripping tags with a blocklist
# VULNERABLE - removing what looks dangerous, rather than encoding
sub clean {
my ($input) = @_;
$input =~ s{<script>}{}gi;
$input =~ s{javascript:}{}gi;
return $input;
}
print "<div>" . clean($q->param('comment')) . "</div>";
Why this is vulnerable: every one of these survives it.
<img src=x onerror=alert(1)> no <script> tag at all
<svg onload=alert(1)> same, different element
<scr<script>ipt>alert(1)</script> the inner match is removed, leaving <script>
javascript:alert(1) the literal "javascript:" never appears
The set of ways to execute script is open-ended and grows with the HTML and
browser specifications, so a blocklist is guessing against a list it cannot
enumerate. The second substitution is worse than useless: with /g, removing
javascript: from javajavascript:script: leaves javascript: behind.
Interpolating into a heredoc or concatenated string
# VULNERABLE - nothing in Perl escapes on interpolation
my $comment = $q->param('comment');
print <<"HTML";
<div class="comment">$comment</div>
HTML
Why this is vulnerable: Perl has no templating layer of its own and no
implicit escaping. String interpolation, . concatenation and qq{} all place
the value into the document verbatim, so every output site is opt-in for
encoding. This is why CWE-80 findings in Perl cluster in hand-built HTML rather
than in template files.
Trusting escaping that is not switched on
# VULNERABLE - TT does not escape unless told to
[% comment %]
# VULNERABLE - Mason escapes only when default_escape_flags is set
<% $comment %>
Why this is vulnerable: Template Toolkit performs no escaping without an
explicit | html filter or a global AUTO_FILTER, and HTML::Mason escapes
only when default_escape_flags => 'h' is configured. Both look like escaping
template engines and neither is one by default, so a page that filters some
variables can give false confidence about the rest.
Secure Patterns
Encode at every output site
escapeHTML() and encode_entities() are the encoders; the worked examples,
including URL and JavaScript context, are on
CWE-79 - Perl.
use CGI;
use HTML::Entities qw(encode_entities);
my $q = CGI->new;
# SECURE - encoded for element content
print "<div class=\"comment\">" . encode_entities($q->param('comment')) . "</div>";
Why this works: encoding converts < to < before the browser parses
the document, so there is no tag left for a blocklist to miss, whatever the
attacker writes. Because Perl never escapes for you, the discipline that
matters is applying this at every site rather than choosing a better filter -
and turning on AUTO_FILTER in Template Toolkit or default_escape_flags in
Mason so the templates stop being opt-in.
When the input really is HTML
If users are meant to submit formatting, encoding it would defeat the feature. Use an allowlist sanitizer, which parses the HTML and rebuilds it from the elements you permit rather than deleting what looks bad:
use HTML::StripScripts::Parser;
# SECURE - allowlist parse-and-rebuild, not a blocklist filter
my $filter = HTML::StripScripts::Parser->new({
Context => 'Flow',
AllowSrc => 0,
AllowHref => 1,
AllowRelURL => 1,
EscapeFiltered => 1,
});
my $safe_html = $filter->filter_html($dirty_html);
Why this works: the open-ended list of ways to run script stops mattering,
because the question becomes "is this element on the list" instead of "is this
string dangerous" - the question a blocklist cannot answer.
AllowSrc => 0 and the URL options control whether attacker-supplied URLs can
appear at all, which is the gap an element allowlist alone would leave.
Install with cpanm HTML::StripScripts::Parser.
Considerations
Whether the finding is CWE-80 or CWE-79 rarely changes the fix. Both are resolved by encoding for the context the value lands in. The label tells you what the scanner matched - tag characters in element content - not the shape of the remediation.
It does change where to look next. CWE-80 only describes element content.
An application that failed to encode there has usually failed in the contexts
CWE-80 does not name: quoted and unquoted attributes, href and src values,
and inline <script> blocks. Those are CWE-79's territory, and one CWE-80
instance is a reason to check them.
In Perl the population is larger than the finding suggests. With no
implicit escaping anywhere in the language, one unencoded interpolation usually
means a file full of them. Treat a CWE-80 hit as a prompt to sweep the script
for print, qq{} and heredocs carrying parameters, rather than to patch the
reported line.
Additional Resources
- CWE-80: Improper Neutralization of Script-Related HTML Tags
- CWE-79 - Perl - the encoders, template configuration and CSP guidance this page defers to
- HTML::StripScripts::Parser
- OWASP XSS Prevention Cheat Sheet