File Coverage

blib/lib/CGI/IDS.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package CGI::IDS;
2              
3             #------------------------- Notes -----------------------------------------------
4             # This source code is documented in both POD and ROBODoc format.
5             # Please find additional POD documentation at the end of this file
6             # (search for "__END__").
7             #-------------------------------------------------------------------------------
8              
9             #****c* IDS
10             # NAME
11             # PerlIDS (CGI::IDS)
12             # DESCRIPTION
13             # Website Intrusion Detection System based on PHPIDS https://phpids.org rev. 1409
14             # AUTHOR
15             # Hinnerk Altenburg
16             # CREATION DATE
17             # 2008-06-03
18             # COPYRIGHT
19             # Copyright (C) 2008-2014 Hinnerk Altenburg
20             #
21             # This file is part of PerlIDS.
22             #
23             # PerlIDS is free software: you can redistribute it and/or modify
24             # it under the terms of the GNU Lesser General Public License as published by
25             # the Free Software Foundation, either version 3 of the License, or
26             # (at your option) any later version.
27             #
28             # PerlIDS is distributed in the hope that it will be useful,
29             # but WITHOUT ANY WARRANTY; without even the implied warranty of
30             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31             # GNU Lesser General Public License for more details.
32             #
33             # You should have received a copy of the GNU Lesser General Public License
34             # along with PerlIDS. If not, see .
35              
36             #****
37              
38             =head1 NAME
39              
40             CGI::IDS - PerlIDS - Perl Website Intrusion Detection System (XSS, CSRF, SQLI, LFI etc.)
41              
42             =head1 VERSION
43              
44             Version 1.0217 - based on and tested against the filter tests of PHPIDS https://phpids.org rev. 1409
45              
46             =cut
47              
48             our $VERSION = '1.0217';
49              
50             =head1 DESCRIPTION
51              
52             PerlIDS (CGI::IDS) is a website intrusion detection system based on PHPIDS L to detect possible attacks in website requests, e.g. Cross-Site Scripting (XSS), Cross-Site Request Forgery (CSRF), SQL Injections (SQLI) etc.
53              
54             It parses any hashref for possible attacks, so it does not depend on CGI.pm.
55              
56             The intrusion detection is based on a set of converters that convert the request according to common techniques that are used to hide attacks. These converted strings are checked for attacks by running a filter set of currently 68 regular expressions and a generic attack detector to find obfuscated attacks. For easily keeping the filter set up-to-date, PerlIDS is compatible to the original XML filter set of PHPIDS, which is frequently updated.
57              
58             Each matching regular expression has it's own impact value that increases the tested string's total attack impact. Using these total impacts, a threshold can be defined by the calling application to log the suspicious requests to database and send out warnings via e-mail or even SMS on high impacts that indicate critical attack activity. These impacts can be summed per IP address, session or user to identify attackers who are testing the website with small impact attacks over a time.
59              
60             You can improve the speed and the accurancy (reduce false positives) of the IDS by specifying an L. This whitelist check can also be processed separately by using L if you want to pre-check the parameters on your application servers before you send only the suspicious requests over to worker servers that do the complete CGI::IDS check.
61              
62             Download and install via CPAN: L
63              
64             Report issues and contribute to PerlIDS on GitHub: L
65              
66             =head1 SYNOPSIS
67              
68             use CGI;
69             use CGI::IDS;
70              
71             $cgi = new CGI;
72              
73             # instantiate the IDS object;
74             # do not scan keys, values only; don't scan PHP code injection filters (IDs 58,59,60);
75             # whitelist the parameters as per given XML whitelist file;
76             # All arguments are optional, 'my $ids = new CGI::IDS();' is also working correctly,
77             # loading the entire shipped filter set and not scanning the keys.
78             # See new() for all possible arguments.
79             my $ids = new CGI::IDS(
80             whitelist_file => '/home/hinnerk/ids/param_whitelist.xml',
81             disable_filters => [58,59,60],
82             );
83              
84             # start detection
85             my %params = $cgi->Vars;
86             my $impact = $ids->detect_attacks( request => \%params );
87              
88             if ($impact > 0) {
89             my_log( $ids->get_attacks() );
90             }
91             if ($impact > 30) {
92             my_warn_user();
93             my_email( $ids->get_attacks() );
94             }
95             if ($impact > 50) {
96             my_deactivate_user();
97             my_sms( $ids->get_attacks() );
98             }
99              
100             # now with scanning the hash keys
101             $ids->set_scan_keys(scan_keys => 1);
102             $impact = $ids->detect_attacks( request => \%params );
103              
104             See F in CGI::IDS module package for a running demo.
105              
106             You might want to build your own 'session impact counter' that increases during multiple suspicious requests by one single user, session or IP address.
107              
108             =head1 METHODS
109              
110             =cut
111              
112             #------------------------- Pragmas ---------------------------------------------
113 1     1   59831 use strict;
  1         3  
  1         42  
114 1     1   5 use warnings;
  1         2  
  1         33  
115              
116             #------------------------- Libs ------------------------------------------------
117 1     1   554 use XML::Simple qw(:strict);
  0            
  0            
118             use HTML::Entities;
119             use MIME::Base64;
120             use Encode qw(decode);
121             use Carp;
122             use Time::HiRes;
123             use FindBin qw($Bin);
124             use CGI::IDS::Whitelist;
125              
126             #------------------------- Settings --------------------------------------------
127             $XML::Simple::PREFERRED_PARSER = "XML::Parser";
128              
129             #------------------------- Debugging -------------------------------------------
130             # debug modes (binary):
131             use constant DEBUG_KEY_VALUES => (1 << 0); # print each key value pair
132             use constant DEBUG_IMPACTS => (1 << 1); # print impact per key value pair
133             use constant DEBUG_ARRAY_INFO => (1 << 2); # print attack info arrays
134             use constant DEBUG_CONVERTERS => (1 << 3); # print output of each converter
135             use constant DEBUG_SORT_KEYS_NUM => (1 << 4); # sort request by keys numerically
136             use constant DEBUG_SORT_KEYS_ALPHA => (1 << 5); # sort request by keys alphabetically
137             use constant DEBUG_WHITELIST => (1 << 6); # dumps loaded whitelist hash
138             use constant DEBUG_MATCHED_FILTERS => (1 << 7); # print IDs of matched filters
139              
140             #use constant DEBUG_MODE => DEBUG_KEY_VALUES |
141             # DEBUG_IMPACTS |
142             # DEBUG_WHITELIST |
143             # DEBUG_ARRAY_INFO |
144             # DEBUG_CONVERTERS |
145             # DEBUG_MATCHED_FILTERS |
146             # DEBUG_SORT_KEYS_NUM;
147              
148             # simply comment this line out to switch debugging mode on (also uncomment above declaration)
149             use constant DEBUG_MODE => 0;
150              
151             #------------------------- Constants -------------------------------------------
152              
153             # converter functions, processed in this order
154             my @CONVERTERS = qw/
155             stripslashes
156             _convert_from_repetition
157             _convert_from_commented
158             _convert_from_whitespace
159             _convert_from_js_charcode
160             _convert_js_regex_modifiers
161             _convert_entities
162             _convert_quotes
163             _convert_from_sql_hex
164             _convert_from_sql_keywords
165             _convert_from_control_chars
166             _convert_from_nested_base64
167             _convert_from_out_of_range_chars
168             _convert_from_xml
169             _convert_from_js_unicode
170             _convert_from_utf7
171             _convert_from_concatenated
172             _convert_from_proprietary_encodings
173             _run_centrifuge
174             /;
175              
176             #------------------------- Subs ------------------------------------------------
177              
178             #****m* IDS/new
179             # NAME
180             # Constructor
181             # DESCRIPTION
182             # Creates an IDS object.
183             # The filter set and whitelist will stay loaded during the lifetime of the object.
184             # You may call detect_attacks() multiple times, the attack array ( get_attacks() )
185             # will be emptied at the start of each run of detect_attacks().
186             # INPUT
187             # HASH
188             # filters_file STRING The path to the filters XML file (defaults to shipped IDS.xml)
189             # whitelist_file STRING The path to the whitelist XML file
190             # scan_keys INT 1 to scan also the keys, 0 if not (default: 0)
191             # disable_filters ARRAYREF[INT,INT,...] if given, these filter ids will be disabled
192             # OUTPUT
193             # IDS object, dies (croaks) if no filter rule could be loaded
194             # EXAMPLE
195             # # instantiate object; do not scan keys, values only
196             # my $ids = new CGI::IDS(
197             # filters_file => '/home/hinnerk/sandbox/ids/cgi-bin/default_filter.xml',
198             # whitelist_file => '/home/hinnerk/sandbox/ids/cgi-bin/param_whitelist.xml',
199             # scan_keys => 0,
200             # disable_filters => [58,59,60],
201             # );
202             #****
203              
204             =head2 new()
205              
206             Constructor. Can optionally take a hash of settings. If I is not given,
207             the shipped filter set will be loaded, I defaults to 0.
208              
209             The filter set and whitelist will stay loaded during the lifetime of the object.
210             You may call C multiple times, the attack array (C)
211             will be emptied at the start of each run of C.
212              
213             For example, the following is a valid constructor:
214              
215             my $ids = new CGI::IDS(
216             filters_file => '/home/hinnerk/ids/default_filter.xml',
217             whitelist_file => '/home/hinnerk/ids/param_whitelist.xml',
218             scan_keys => 0,
219             disable_filters => [58,59,60],
220             );
221              
222             The Constructor dies (croaks) if no filter rule could be loaded.
223              
224             =cut
225              
226             sub new {
227             my ($package, %args) = @_;
228              
229             # defaults
230             $args{scan_keys} = $args{scan_keys} ? 1 : 0;
231             my $filters_file_default = __FILE__;
232             $filters_file_default =~ s/IDS.pm/IDS.xml/;
233              
234             # self member variables
235             my $self = {
236             filters_file => $args{filters_file} || $filters_file_default,
237             whitelist => CGI::IDS::Whitelist->new(whitelist_file => $args{whitelist_file}),
238             scan_keys => $args{scan_keys},
239             impact => 0,
240             attacks => undef, # []
241             filters => [],
242             filter_disabled => { map { $_ => 1} @{$args{disable_filters} || []} },
243             };
244              
245             if (DEBUG_MODE & DEBUG_WHITELIST) {
246             use Data::Dumper; print Dumper($self->{whitelist}->{whitelist});
247             }
248              
249             # create object
250             bless $self, $package;
251              
252             # read & parse filter XML
253             if (!$self->_load_filters_from_xml($self->{filters_file})) {
254             croak "No IDS filter rules loaded!";
255             }
256              
257             return $self;
258             }
259              
260             #****m* IDS/detect_attacks
261             # NAME
262             # detect_attacks
263             # DESCRIPTION
264             # Parses a hashref (e.g. $query->Vars) for detection of possible attacks.
265             # The attack array is emptied at the start of each run.
266             # INPUT
267             # +request hashref to be parsed
268             # OUTPUT
269             # Impact if filter matched, 0 otherwise
270             # SYNOPSIS
271             # $ids->detect_attacks(request => $query->Vars);
272             #****
273              
274             =head2 detect_attacks()
275              
276             DESCRIPTION
277             Parses a hashref (e.g. $query->Vars) for detection of possible attacks.
278             The attack array is emptied at the start of each run.
279             INPUT
280             +request hashref to be parsed
281             OUTPUT
282             Impact if filter matched, 0 otherwise
283             SYNOPSIS
284             $ids->detect_attacks(request => $query->Vars);
285              
286             =cut
287              
288             sub detect_attacks {
289             my ($self, %args) = @_;
290              
291             return 0 unless ($args{request});
292             my $request = $args{request};
293              
294             # reset last detection data
295             $self->{impact} = 0;
296             $self->{attacks} = [];
297             $self->{filtered_keys} = [];
298             $self->{non_filtered_keys} = [];
299              
300             my @request_keys = keys %$request;
301             # sorting for filter debugging only
302             if (DEBUG_MODE & DEBUG_SORT_KEYS_ALPHA) {
303             @request_keys = sort {$a cmp $b} @request_keys;
304             }
305             elsif (DEBUG_MODE & DEBUG_SORT_KEYS_NUM) {
306             @request_keys = sort {$a <=> $b} @request_keys;
307             }
308              
309             foreach my $key (@request_keys) {
310             my $filter_impact = 0;
311             my $key_converted = '';
312             my $value_converted = '';
313             my $time_ms = 0;
314             my @matched_filters = ();
315             my @matched_tags = ();
316              
317             my $request_value = defined $request->{$key} ? $request->{$key} : '';
318              
319             if (DEBUG_MODE & DEBUG_KEY_VALUES) {
320             print "\n\n\n******************************************\n".
321             "Key : $key\nValue : $request_value\n";
322             }
323              
324             if ($self->{whitelist}->is_suspicious(key => $key, request => $request)) {
325             $request_value = $self->{whitelist}->convert_if_marked_encoded(key => $key, value => $request_value);
326             my $attacks = $self->_apply_filters($request_value);
327             if ($attacks->{impact}) {
328             $filter_impact += $attacks->{impact};
329             $time_ms += $attacks->{time_ms};
330             $value_converted = $attacks->{string_converted};
331             push (@matched_filters, @{$attacks->{filters}});
332             push (@matched_tags, @{$attacks->{tags}});
333             }
334             }
335              
336             # scan key only if desired
337             if ($self->{scan_keys}) {
338             # scan only if value is not harmless
339             if ( !$self->{whitelist}->is_harmless_string($key) ) {
340             # apply filters to key
341             my $attacks = $self->_apply_filters($key);
342             $filter_impact += $attacks->{impact};
343             $time_ms += $attacks->{time_ms};
344             $key_converted = $attacks->{string_converted};
345             push (@matched_filters, @{$attacks->{filters}});
346             push (@matched_tags, @{$attacks->{tags}});
347             }
348             else {
349             # skipped, alphanumeric key only
350             }
351             }
352              
353             # add attack to log
354             my %attack = ();
355             if ($filter_impact) {
356             # make arrays unique and sorted
357             my %seen = ();
358             @matched_filters = sort grep { ! $seen{$_} ++ } @matched_filters;
359             %seen = ();
360             @matched_tags = sort grep { ! $seen{$_} ++ } @matched_tags;
361              
362             %attack = (
363             key => $key,
364             key_converted => $key_converted,
365             value => $request_value,
366             value_converted => $value_converted,
367             time_ms => $time_ms,
368             impact => $filter_impact,
369             matched_filters => \@matched_filters,
370             matched_tags => \@matched_tags,
371             );
372             push (@{$self->{attacks}}, \%attack);
373             }
374             $self->{impact} += $filter_impact;
375              
376             if (DEBUG_MODE & DEBUG_ARRAY_INFO && %attack) {
377             use Data::Dumper;
378             print "------------------------------------------\n".
379             Dumper(\%attack) .
380             "\n\n";
381             }
382              
383             if (DEBUG_MODE & DEBUG_MATCHED_FILTERS && @matched_filters) {
384             my $filters_concat = join ", ", @matched_filters;
385             print "Filters: $filters_concat\n";
386             }
387              
388             if (DEBUG_MODE & DEBUG_IMPACTS) {
389             print "Impact : $filter_impact\n";
390             }
391              
392             } # end of foreach key
393             push (@{$self->{filtered_keys}}, @{$self->{whitelist}->suspicious_keys()});
394             push (@{$self->{non_filtered_keys}}, @{$self->{whitelist}->non_suspicious_keys()});
395             # reset filtered_keys and non_filtered_keys
396             $self->{whitelist}->reset();
397              
398             if ($self->{impact} > 0) {
399             return $self->{impact};
400             }
401             else {
402             return 0;
403             }
404             }
405              
406             #****m* IDS/set_scan_keys
407             # NAME
408             # set_scan_keys
409             # DESCRIPTION
410             # Sets key scanning option
411             # INPUT
412             # +scan_keys 1 to scan keys, 0 to switch off scanning keys, defaults to 0
413             # OUTPUT
414             # none
415             # SYNOPSIS
416             # $ids->set_scan_keys(scan_keys => 1);
417             #****
418              
419             =head2 set_scan_keys()
420              
421             DESCRIPTION
422             Sets key scanning option
423             INPUT
424             +scan_keys 1 to scan keys, 0 to switch off scanning keys, defaults to 0
425             OUTPUT
426             none
427             SYNOPSIS
428             $ids->set_scan_keys(scan_keys => 1);
429              
430             =cut
431              
432             sub set_scan_keys {
433             my ($self, %args) = @_;
434              
435             $self->{scan_keys} = $args{scan_keys} ? 1 : 0;
436             }
437              
438             #****m* IDS/get_attacks
439             # NAME
440             # get_attacks
441             # DESCRIPTION
442             # Get an key/value/impact array of all detected attacks.
443             # The array is emptied at the start of each run of detect_attacks().
444             # INPUT
445             # none
446             # OUTPUT
447             # HASHREF (
448             # key => '',
449             # value => '',
450             # impact => n,
451             # filters => (n, n, n, n, ...),
452             # tags => ('', '', '', '', ...),
453             # )
454             # SYNOPSIS
455             # $ids->get_attacks();
456             #****
457              
458             =head2 get_attacks()
459              
460             DESCRIPTION
461             Get an key/value/impact array of all detected attacks.
462             The array is emptied at the start of each run of C.
463             INPUT
464             none
465             OUTPUT
466             ARRAY (
467             key => '',
468             value => '',
469             impact => n,
470             filters => (n, n, n, n, ...),
471             tags => ('', '', '', '', ...),
472             )
473             SYNOPSIS
474             $ids->get_attacks();
475              
476             =cut
477              
478             sub get_attacks {
479             my ($self) = @_;
480              
481             return $self->{attacks};
482             }
483              
484             #****m* IDS/get_rule_description
485             # NAME
486             # get_rule_description
487             # DESCRIPTION
488             # This sub returns the rule description for a given rule id. This can be used for logging purposes.
489             # INPUT
490             # HASH
491             # + rule_id id of rule
492             # OUTPUT
493             # SCALAR description
494             # EXAMPLE
495             # $ids->get_rule_description( rule_id => $rule_id );
496             #****
497              
498             =head2 get_rule_description()
499              
500             DESCRIPTION
501             Returns the rule description for a given rule id. This can be used for logging purposes.
502             INPUT
503             HASH
504             + rule_id id of rule
505             OUTPUT
506             SCALAR description
507             EXAMPLE
508             $ids->get_rule_description( rule_id => $rule_id );
509              
510             =cut
511              
512             sub get_rule_description {
513             my ($self, %args) = @_;
514             return $self->{rule_descriptions}{$args{rule_id}};
515             }
516              
517             #****im* IDS/_apply_filters
518             # NAME
519             # _apply_filters
520             # DESCRIPTION
521             # Applies filter rules to a string to detect attacks
522             # INPUT
523             # + $string string to be checked for possible attacks
524             # OUTPUT
525             # attack hashref:
526             # (
527             # impact => n,
528             # filters => (n, n, n, ...),
529             # tags => ('', '', '', ...),
530             # string_converted => string
531             # )
532             # SYNOPSIS
533             # IDS::_apply_filters($string);
534             #****
535              
536             sub _apply_filters {
537             my ($self, $string) = @_;
538             my %attack = (
539             filters => [],
540             tags => [],
541             impact => 0,
542             string_converted => '',
543             );
544              
545             # benchmark
546             my $start_time = Time::HiRes::time();
547              
548             # make UTF-8 and sanitize from malformated UTF-8, if necessary
549             $string = $self->{whitelist}->make_utf_8($string);
550              
551             # run all string converters
552             $attack{string_converted} = _run_all_converters($string);
553              
554             # apply filters
555             foreach my $filter (@{$self->{filters}}) {
556              
557             # skip disabled filters
558             next if ($self->{filter_disabled}{$filter->{id}});
559             my $string_converted_lc = lc($attack{string_converted});
560             if ($string_converted_lc =~ $filter->{rule}) {
561             $attack{impact} += $filter->{impact};
562             push (@{$attack{filters}}, $filter->{id});
563             push (@{$attack{tags}}, @{$filter->{tags}});
564             }
565             }
566              
567             # benchmark
568             my $end_time = Time::HiRes::time();
569             $attack{time_ms} = int(($end_time-$start_time)*1000);
570              
571             return \%attack;
572             }
573              
574             #****im* IDS/_load_filters_from_xml
575             # NAME
576             # _load_filters_from_xml
577             # DESCRIPTION
578             # loads the filters from PHPIDS filter XML file
579             # INPUT
580             # filterfile path + name of the XML filter file
581             # OUTPUT
582             # filtercount number of loaded filters
583             # SYNOPSIS
584             # IDS::_load_filters_from_xml('/home/xyz/default_filter.xml');
585             #****
586              
587             sub _load_filters_from_xml {
588             my ($self, $filterfile) = @_;
589             my $filtercnt = 0;
590              
591             if ($filterfile) {
592             # read & parse filter XML
593             my $filterxml;
594             eval {
595             $filterxml = XML::Simple::XMLin($filterfile,
596             forcearray => [ qw(rule description tags tag impact filter filters)],
597             keyattr => [],
598             );
599             };
600             if ($@) {
601             croak "Error in _load_filters_from_xml while parsing $filterfile: $@";
602             }
603              
604             # convert XML structure into handy data structure
605             foreach my $filterobj (@{$filterxml->{filter}}) {
606             my @taglist = ();
607             foreach my $tag (@{$filterobj->{tags}[0]->{tag}}) {
608             push(@taglist, $tag);
609             }
610              
611             my $rule = '';
612             eval {
613             $rule = qr/$filterobj->{rule}[0]/ms;
614             };
615             if ($@) {
616             croak 'Error in filter rule #' . $filterobj->{id} . ': ' . $filterobj->{rule}[0] . ' Message: ' . $@;
617             }
618             my %filterhash = (
619             rule => $rule,
620             impact => $filterobj->{impact}[0],
621             id => $filterobj->{id},
622             tags => \@taglist,
623             );
624             push (@{$self->{filters}}, \%filterhash);
625             $self->{rule_descriptions}{$filterobj->{id}} = $filterobj->{description}[0];
626             $filtercnt++
627             }
628             }
629             return $filtercnt;
630             }
631              
632             #****if* IDS/_run_all_converters
633             # NAME
634             # _run_all_converters
635             # DESCRIPTION
636             # Runs all converter functions
637             # INPUT
638             # value the string to convert
639             # OUTPUT
640             # value converted string
641             # SYNOPSIS
642             # IDS::_run_all_converters($value);
643             #****
644              
645             sub _run_all_converters {
646             my ($value) = @_;
647             if (DEBUG_MODE & DEBUG_CONVERTERS) {
648             print "------------------------------------------\n\n";
649             }
650              
651             foreach my $converter (@CONVERTERS) {
652             no strict 'refs';
653             $value = $converter->($value);
654             if (DEBUG_MODE & DEBUG_CONVERTERS) {
655             print "$converter output:\n$value\n\n";
656             }
657             }
658             return $value;
659             }
660              
661             #****if* IDS/_convert_from_repetition
662             # NAME
663             # _convert_from_repetition
664             # DESCRIPTION
665             # Make sure the value to normalize and monitor doesn't contain
666             # possibilities for a regex DoS.
667             # INPUT
668             # value the value to pre-sanitize
669             # OUTPUT
670             # value converted string
671             # SYNOPSIS
672             # IDS::_convert_from_repetition($value);
673             #****
674              
675             sub _convert_from_repetition {
676             my ($value) = @_;
677              
678             # remove obvios repetition patterns
679             $value = preg_replace(
680             qr/(?:(.{2,})\1{32,})|(?:[+=|\-@\s]{128,})/,
681             'x',
682             $value
683             );
684             return $value;
685             }
686              
687             #****if* IDS/_convert_from_commented
688             # NAME
689             # _convert_from_commented
690             # DESCRIPTION
691             # Check for comments and erases them if available
692             # INPUT
693             # value the string to convert
694             # OUTPUT
695             # value converted string
696             # SYNOPSIS
697             # IDS::_convert_from_commented($value);
698             #****
699              
700             sub _convert_from_commented {
701             my ($value) = @_;
702              
703             # check for existing comments
704             if (preg_match(qr/(?:\|\/\*|\*\/|\/\/\W*\w+\s*$)|(?:--[^-]*-)/ms, $value)) { #/
705              
706             my @pattern = (
707             qr/(?:(?:))/ms,
708             qr/(?:(?:\/\*\/*[^\/\*]*)+\*\/)/ms,
709             qr/(?:--[^-]*-)/ms,
710             );
711              
712             my $converted = preg_replace(\@pattern, ';', $value);
713             $value .= "\n" . $converted;
714             }
715              
716             # make sure inline comments are detected and converted correctly
717             $value = preg_replace(qr/(<\w+)\/+(\w+=?)/m, '$1/$2', $value);
718             $value = preg_replace(qr/[^\\:]\/\/(.*)$/m, '/**/$1', $value);
719              
720             return $value;
721             }
722              
723             #****if* IDS/_convert_from_whitespace
724             # NAME
725             # _convert_from_whitespace
726             # DESCRIPTION
727             # Strip newlines
728             # INPUT
729             # value the string to convert
730             # OUTPUT
731             # value converted string
732             # SYNOPSIS
733             # IDS::_convert_from_whitespace($value);
734             #****
735              
736             sub _convert_from_whitespace {
737             my ($value) = @_;
738              
739             # check for inline linebreaks
740             my @search = ('\r', '\n', '\f', '\t', '\v');
741             $value = str_replace(\@search, ';', $value);
742              
743             # replace replacement characters regular spaces
744             $value = str_replace('�', ' ', $value);
745              
746             # convert real linebreaks (\013 in Perl instead of \v in PHP et al.)
747             return preg_replace(qr/(?:\n|\r|\013)/m, ' ', $value);
748             }
749              
750             #****if* IDS/_convert_from_js_charcode
751             # NAME
752             # _convert_from_js_charcode
753             # DESCRIPTION
754             # Checks for common charcode pattern and decodes them
755             # INPUT
756             # value the string to convert
757             # OUTPUT
758             # value converted string
759             # SYNOPSIS
760             # IDS::_convert_from_js_charcode($value);
761             #****
762              
763             sub _convert_from_js_charcode {
764             my ($value) = @_;
765              
766             my @matches = ();
767              
768             # check if value matches typical charCode pattern
769             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
770             if (preg_match_all(qr/(?:[\d+-=\/\* ]+(?:\s?,\s?[\d+-=\/\* ]+)){4,}/ms,
771             $value, \@matches)) {
772             my $converted = '';
773             my $string = implode(',', $matches[0]);
774             $string = preg_replace(qr/\s/, '', $string);
775             $string = preg_replace(qr/\w+=/, '', $string);
776             my @charcode = explode(',', $string);
777              
778             foreach my $char (@charcode) {
779             $char = preg_replace(qr/\W0/s, '', $char);
780              
781             my @matches = ();
782             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
783             if (preg_match_all(qr/(\d*[+-\/\* ]\d+)/, $char, \@matches)) {
784             my @match = split(qr/(\W?\d+)/,
785             (implode('', $matches[0])),
786             # null,
787             # PREG_SPLIT_DELIM_CAPTURE
788             );
789             # 3rd argument null, 4th argument PREG_SPLIT_DELIM_CAPTURE is default in Perl and not there
790             my $test = implode('', $matches[0]);
791              
792             if (array_sum(@match) >= 20 && array_sum(@match) <= 127) {
793             $converted .= chr(array_sum(@match));
794             }
795              
796             }
797             elsif ($char && $char >= 20 && $char <= 127) {
798             $converted .= chr($char);
799             }
800             }
801              
802             $value .= "\n" . $converted;
803             }
804              
805             # check for octal charcode pattern
806             # PHP to Perl note: \\ in Perl instead of \\\ in PHP
807             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
808             if (preg_match_all(qr/((?:(?:[\\]+\d+\s*){8,}))/ms, $value, \@matches)) {
809             my $converted = '';
810             my @charcode = explode('\\', preg_replace(qr/\s/, '', implode(',',
811             $matches[0])));
812              
813             foreach my $char (@charcode) {
814             if ($char) {
815             if (oct($char) >= 20 && oct($char) <= 127) {
816             $converted .= chr(oct($char));
817             }
818             }
819             }
820             $value .= "\n" . $converted;
821             }
822              
823             # check for hexadecimal charcode pattern
824             # PHP to Perl note: \\ in Perl instead of \\\ in PHP
825             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
826             if (preg_match_all(qr/((?:(?:[\\]+\w+[ \t]*){8,}))/ms, $value, \@matches)) {
827             my $converted = '';
828             my @charcode = explode('\\', preg_replace(qr/[ux]/, '', implode(',',
829             $matches[0])));
830              
831             foreach my $char (@charcode) {
832             if ($char) {
833             if (hex($char) >= 20 && hex($char) <= 127) {
834             $converted .= chr(hex($char));
835             }
836             }
837             }
838             $value .= "\n" . $converted;
839             }
840              
841             return $value;
842              
843             }
844              
845             #****if* IDS/_convert_js_regex_modifiers
846             # NAME
847             # _convert_js_regex_modifiers
848             # DESCRIPTION
849             # Eliminate JS regex modifiers
850             # INPUT
851             # value the string to convert
852             # OUTPUT
853             # value converted string
854             # SYNOPSIS
855             # IDS::_convert_js_regex_modifiers($value);
856             #****
857              
858             sub _convert_js_regex_modifiers {
859             my ($value) = @_;
860              
861             $value = preg_replace(qr/\/[gim]+/, '/', $value);
862             return $value;
863             }
864              
865             #****if* IDS/_convert_quotes
866             # NAME
867             # _convert_quotes
868             # DESCRIPTION
869             # Normalize quotes
870             # INPUT
871             # value the string to convert
872             # OUTPUT
873             # value converted string
874             # SYNOPSIS
875             # IDS::_convert_quotes($value);
876             #****
877              
878             sub _convert_quotes {
879             my ($value) = @_;
880              
881             # normalize different quotes to "
882             my @pattern = ('\'', '`', '´', '’', '‘');
883             $value = str_replace(\@pattern, '"', $value);
884              
885             # make sure harmless quoted strings don't generate false alerts
886             $value = preg_replace(qr/^"([^"=\\!><~]+)"$/, '$1', $value);
887             return $value;
888             }
889              
890             #****if* IDS/_convert_from_sql_hex
891             # NAME
892             # _convert_from_sql_hex
893             # DESCRIPTION
894             # Converts SQLHEX to plain text
895             # INPUT
896             # value the string to convert
897             # OUTPUT
898             # value converted string
899             # SYNOPSIS
900             # IDS::_convert_from_sql_hex($value);
901             #****
902              
903             sub _convert_from_sql_hex {
904             my ($value) = @_;
905              
906             my @matches = ();
907             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
908             if(preg_match_all(qr/((?:0x[a-f\d]{2,}[a-f\d]*)+)/im, $value, \@matches)) {
909             foreach my $match ($matches[0]) {
910             my $converted = '';
911             foreach my $hex_index (str_split($match, 2)) {
912             if(preg_match(qr/[a-f\d]{2,3}/i, $hex_index)) {
913             $converted .= chr(hex($hex_index));
914             }
915             }
916             $value = str_replace($match, $converted, $value);
917             }
918             }
919             # take care of hex encoded ctrl chars
920             $value = preg_replace('/0x\d+/m', 1, $value);
921              
922             return $value;
923             }
924              
925             #****if* IDS/_convert_from_sql_keywords
926             # NAME
927             # _convert_from_sql_keywords
928             # DESCRIPTION
929             # Converts basic SQL keywords and obfuscations
930             # INPUT
931             # value the string to convert
932             # OUTPUT
933             # value converted string
934             # SYNOPSIS
935             # IDS::_convert_from_sql_keywords($value);
936             #****
937              
938             sub _convert_from_sql_keywords {
939             my ($value) = @_;
940              
941             my $pattern = qr/(?:IS\s+null)|(LIKE\s+null)|(?:(?:^|\W)IN[+\s]*\([\s\d"]+[^()]*\))/ims;
942             $value = preg_replace($pattern, '"=0', $value);
943             $value = preg_replace(qr/\W+\s*like\s*\W+/ims, '1" OR "1"', $value);
944             $value = preg_replace(qr/null[,"\s]/ims, ',0', $value);
945             $value = preg_replace(qr/\d+\./ims, ' 1', $value);
946             $value = preg_replace(qr/,null/ims, ',0', $value);
947             $value = preg_replace(qr/(?:between|mod)/ims, 'or', $value);
948             $value = preg_replace(qr/(?:and\s+\d+\.?\d*)/ims, '', $value);
949             $value = preg_replace(qr/(?:\s+and\s+)/ims, ' or ', $value);
950             # \\N instead of PHP's \\\N
951             $pattern = qr/[^\w,\(]NULL|\\N|TRUE|FALSE|UTC_TIME|LOCALTIME(?:STAMP)?|CURRENT_\w+|BINARY|(?:(?:ASCII|SOUNDEX|FIND_IN_SET|MD5|R?LIKE)[+\s]*\([^()]+\))|(?:-+\d)/ims;
952             $value = preg_replace($pattern, 0, $value);
953             $pattern = qr/(?:NOT\s+BETWEEN)|(?:IS\s+NOT)|(?:NOT\s+IN)|(?:XOR|\WDIV\W|\WNOT\W|<>|RLIKE(?:\s+BINARY)?)|(?:REGEXP\s+BINARY)|(?:SOUNDS\s+LIKE)/ims;
954             $value = preg_replace($pattern, '!', $value);
955             $value = preg_replace(qr/"\s+\d/, '"', $value);
956             $value = preg_replace(qr/\/(?:\d+|null)/, '', $value);
957              
958             return $value;
959             }
960              
961             #****if* IDS/_convert_entities
962             # NAME
963             # _convert_entities
964             # DESCRIPTION
965             # Converts from hex/dec entities (use HTML::Entities;)
966             # INPUT
967             # value the string to convert
968             # OUTPUT
969             # value converted string
970             # SYNOPSIS
971             # IDS::_convert_entities($value);
972             #****
973              
974             sub _convert_entities {
975             my ($value) = @_;
976             my $converted = '';
977              
978             # deal with double encoded payload
979             $value = preg_replace(qr/&/, '&', $value);
980              
981             if (preg_match(qr/&#x?[\w]+/ms, $value)) {
982             $converted = preg_replace(qr/(&#x?[\w]{2}\d?);?/ms, '$1;', $value);
983             $converted = HTML::Entities::decode_entities($converted);
984             $value .= "\n" . str_replace(';;', ';', $converted);
985             }
986              
987             # normalize obfuscated protocol handlers
988             $value = preg_replace(
989             '/(?:j\s*a\s*v\s*a\s*s\s*c\s*r\s*i\s*p\s*t\s*)|(d\s*a\s*t\s*a\s*)/ms',
990             'javascript', $value
991             );
992              
993             return $value;
994             }
995              
996             #****if* IDS/_convert_from_control_chars
997             # NAME
998             # _convert_from_control_chars
999             # DESCRIPTION
1000             # Detects nullbytes and controls chars via ord()
1001             # INPUT
1002             # value the string to convert
1003             # OUTPUT
1004             # value converted string
1005             # SYNOPSIS
1006             # IDS::_convert_from_control_chars($value);
1007             #****
1008              
1009             sub _convert_from_control_chars {
1010             my ($value) = @_;
1011              
1012             # critical ctrl values
1013             my @search = (
1014             chr(0), chr(1), chr(2), chr(3), chr(4), chr(5),
1015             chr(6), chr(7), chr(8), chr(11), chr(12), chr(14),
1016             chr(15), chr(16), chr(17), chr(18), chr(19), chr(24),
1017             chr(25), chr(192), chr(193), chr(238), chr(255)
1018             );
1019             $value = str_replace(\@search, '%00', $value);
1020              
1021             # take care for malicious unicode characters
1022             $value = urldecode(preg_replace(qr/(?:%E(?:2|3)%8(?:0|1)%(?:A|8|9)\w|%EF%BB%BF|%EF%BF%BD)|(?:&#(?:65|8)\d{3};?)/i, '',
1023             urlencode($value)));
1024              
1025             $value = urldecode(
1026             preg_replace(qr/(?:%F0%80%BE)/i, '>', urlencode($value)));
1027             $value = urldecode(
1028             preg_replace(qr/(?:%F0%80%BC)/i, '<', urlencode($value)));
1029             $value = urldecode(
1030             preg_replace(qr/(?:%F0%80%A2)/i, '"', urlencode($value)));
1031             $value = urldecode(
1032             preg_replace(qr/(?:%F0%80%A7)/i, '\'', urlencode($value)));
1033              
1034             $value = preg_replace(qr/(?:%ff1c)/, '<', $value);
1035             $value = preg_replace(
1036             qr/(?:&[#x]*(200|820|200|820|zwn?j|lrm|rlm)\w?;?)/i, '', $value
1037             );
1038              
1039             $value = preg_replace(qr/(?:&#(?:65|8)\d{3};?)|(?:&#(?:56|7)3\d{2};?)|(?:&#x(?:fe|20)\w{2};?)|(?:&#x(?:d[c-f])\w{2};?)/i, '',
1040             $value);
1041              
1042             $value = str_replace(
1043             ["\x{ab}", "\x{3008}", "\x{ff1c}", "\x{2039}", "\x{2329}", "\x{27e8}"], '<', $value
1044             );
1045             $value = str_replace(
1046             ["\x{bb}", "\x{3009}", "\x{ff1e}", "\x{203a}", "\x{232a}", "\x{27e9}"], '>', $value
1047             );
1048              
1049             return $value;
1050             }
1051              
1052             #****if* IDS/_convert_from_nested_base64
1053             # NAME
1054             # _convert_from_nested_base64
1055             # DESCRIPTION
1056             # Matches and translates base64 strings and fragments used in data URIs (use MIME::Base64;)
1057             # INPUT
1058             # value the string to convert
1059             # OUTPUT
1060             # value converted string
1061             # SYNOPSIS
1062             # IDS::_convert_from_nested_base64($value);
1063             #****
1064              
1065             sub _convert_from_nested_base64 {
1066             my ($value) = @_;
1067              
1068             my @matches = ();
1069             preg_match_all(qr/(?:^|[,&?])\s*([a-z0-9]{30,}=*)(?:\W|$)/im, #)/
1070             $value,
1071             \@matches,
1072             );
1073             # PHP to Perl note: PHP's $matches[1] is Perl's default ($matches[0] is the entire RegEx match)
1074             foreach my $item (@matches) {
1075             if ($item && !preg_match(qr/[a-f0-9]{32}/i, $item)) {
1076              
1077             # fill up the string with zero bytes if too short for base64 blocks
1078             my $item_original = $item;
1079             if (my $missing_bytes = length($item) % 4) {
1080             for (1..$missing_bytes) {
1081             $item .= "=";
1082             }
1083             }
1084              
1085             my $base64_item = MIME::Base64::decode_base64($item);
1086             $value = str_replace($item_original, $base64_item, $value);
1087             }
1088             }
1089              
1090             return $value;
1091             }
1092              
1093             #****if* IDS/_convert_from_out_of_range_chars
1094             # NAME
1095             # _convert_from_out_of_range_chars
1096             # DESCRIPTION
1097             # Detects nullbytes and controls chars via ord()
1098             # INPUT
1099             # value the string to convert
1100             # OUTPUT
1101             # value converted string
1102             # SYNOPSIS
1103             # IDS::_convert_from_out_of_range_chars($value);
1104             #****
1105              
1106             sub _convert_from_out_of_range_chars {
1107             my ($value) = @_;
1108              
1109             my @values = str_split($value);
1110             foreach my $item (@values) {
1111             if (ord($item) >= 127) {
1112             $value = str_replace($item, ' ', $value);
1113             }
1114             }
1115              
1116             return $value;
1117             }
1118              
1119             #****if* IDS/_convert_from_xml
1120             # NAME
1121             # _convert_from_xml
1122             # DESCRIPTION
1123             # Strip XML patterns
1124             # INPUT
1125             # value the string to convert
1126             # OUTPUT
1127             # value converted string
1128             # SYNOPSIS
1129             # IDS::_convert_from_xml($value);
1130             #****
1131              
1132             sub _convert_from_xml {
1133             my ($value) = @_;
1134              
1135             my $converted = strip_tags($value);
1136              
1137             if ($converted && ($converted ne $value)) {
1138             return $value . "\n" . $converted;
1139             }
1140             return $value;
1141             }
1142              
1143             #****if* IDS/_convert_from_js_unicode
1144             # NAME
1145             # _convert_from_js_unicode
1146             # DESCRIPTION
1147             # Converts JS unicode code points to regular characters
1148             # INPUT
1149             # value the string to convert
1150             # OUTPUT
1151             # value converted string
1152             # SYNOPSIS
1153             # IDS::_convert_from_js_unicode($value);
1154             #****
1155              
1156             sub _convert_from_js_unicode {
1157             my ($value) = @_;
1158             my @matches = ();
1159              
1160             # \\u instead of PHP's \\\u
1161             # PHP to Perl note: additional parenthesis around RegEx for getting PHP's $matches[0]
1162             preg_match_all(qr/(\\u[0-9a-f]{4})/ims, $value, \@matches);
1163              
1164             if ($matches[0]) {
1165             foreach my $match ($matches[0]) {
1166             my $chr = chr(hex(substr($match, 2, 4)));
1167             $value = str_replace($match, $chr, $value);
1168             }
1169             $value .= "\n".'\u0001';
1170             }
1171             return $value;
1172             }
1173              
1174             #****if* IDS/_convert_from_utf7
1175             # NAME
1176             # _convert_from_utf7
1177             # DESCRIPTION
1178             # Converts relevant UTF-7 tags to UTF-8 (use Encode qw/decode/;)
1179             # INPUT
1180             # value the string to convert
1181             # OUTPUT
1182             # value converted string
1183             # SYNOPSIS
1184             # IDS::_convert_from_utf7($value);
1185             #****
1186              
1187             sub _convert_from_utf7 {
1188             my ($value) = @_;
1189              
1190             if (preg_match(qr/\+A\w+-/m, $value)) {
1191             $value .= "\n" . decode("UTF-7", $value);
1192             }
1193              
1194             return $value;
1195             }
1196              
1197             #****if* IDS/_convert_from_concatenated
1198             # NAME
1199             # _convert_from_concatenated
1200             # DESCRIPTION
1201             # Converts basic concatenations
1202             # INPUT
1203             # value the string to convert
1204             # OUTPUT
1205             # value converted string
1206             # SYNOPSIS
1207             # IDS::_convert_from_concatenated($value);
1208             #****
1209              
1210             sub _convert_from_concatenated {
1211             my ($value) = @_;
1212              
1213             # normalize remaining backslashes
1214             # Perl's \\ should be equivalent to PHP's \\\
1215             if ($value ne preg_replace(qr/(?:(\w)\\)/, '$1', $value)) {
1216             $value .= preg_replace(qr/(?:(\w)\\)/, '$1', $value);
1217             }
1218              
1219             my $compare = stripslashes($value);
1220              
1221             my @pattern = (
1222             qr/(?:<\/\w+>\+<\w+>)/s,
1223             qr/(?:":\d+[^"[]+")/s,
1224             qr/(?:"?"\+\w+\+")/s,
1225             qr/(?:"\s*;[^"]+")|(?:";[^"]+:\s*")/s,
1226             qr/(?:"\s*(?:;|\+).{8,18}:\s*")/s,
1227             qr/(?:";\w+=)|(?:!""&&")|(?:~)/s,
1228             qr/(?:"?"\+""?\+?"?)|(?:;\w+=")|(?:"[|&]{2,})/s,
1229             qr/(?:"\s*\W+")/s,
1230             qr/(?:";\w\s*\+=\s*\w?\s*")/s,
1231             qr/(?:"[|&;]+\s*[^|&\n]*[|&]+\s*"?)/s,
1232             qr/(?:";\s*\w+\W+\w*\s*[|&]*")/s,
1233             qr/(?:"\s*"\s*\.)/s,
1234             qr/(?:\s*new\s+\w+\s*[+",])/,
1235             qr/(?:(?:^|\s+)(?:do|else)\s+)/,
1236             qr/(?:[{(]\s*new\s+\w+\s*[)}])/,
1237             qr/(?:(this|self)\.)/,
1238             qr/(?:undefined)/,
1239             qr/(?:in\s+)/,
1240             );
1241              
1242             # strip out concatenations
1243             my $converted = preg_replace(\@pattern, '', $compare);
1244              
1245             # strip object traversal
1246             $converted = preg_replace(qr/\w(\.\w\()/, '$1', $converted);
1247              
1248             # normalize obfuscated method calls
1249             $converted = preg_replace(qr/\)\s*\+/, ')', $converted);
1250              
1251             # convert JS special numbers
1252             $converted = preg_replace(qr/(?:\(*[.\d]e[+-]*[^a-z\W]+\)*)|(?:NaN|Infinity)\W/ims, 1, $converted);
1253              
1254             if ($converted && ($compare ne $converted)) {
1255             $value .= "\n" . $converted;
1256             }
1257              
1258             return $value;
1259             }
1260              
1261             #****if* IDS/_convert_from_proprietary_encodings
1262             # NAME
1263             # _convert_from_proprietary_encodings
1264             # DESCRIPTION
1265             # Collects and decodes proprietary encoding types
1266             # INPUT
1267             # value the string to convert
1268             # OUTPUT
1269             # value converted string
1270             # SYNOPSIS
1271             # IDS::_convert_from_proprietary_encodings($value);
1272             #****
1273              
1274             sub _convert_from_proprietary_encodings {
1275             my ($value) = @_;
1276              
1277             # Xajax error reportings
1278             $value = preg_replace(qr//im, '$1', $value);
1279              
1280             # strip false alert triggering apostrophes
1281             $value = preg_replace(qr/(\w)\"(s)/m, '$1$2', $value);
1282              
1283             # strip quotes within typical search patterns
1284             $value = preg_replace(qr/^"([^"=\\!><~]+)"$/, '$1', $value);
1285              
1286             # OpenID login tokens
1287             $value = preg_replace(qr/{[\w-]{8,9}\}(?:\{[\w=]{8}\}){2}/, '', $value);
1288              
1289             # convert Content to null to avoid false alerts
1290             $value = preg_replace(qr/Content|\Wdo\s/, '', $value);
1291              
1292             # strip emoticons
1293             $value = preg_replace(qr/(?:\s[:;]-[)\/PD]+)|(?:\s;[)PD]+)|(?:\s:[)PD]+)|-\.-|\^\^/m, '', $value);
1294              
1295             # normalize separation char repetition
1296             $value = preg_replace(qr/([.+~=*_\-;])\1{2,}/m, '$1', $value);
1297              
1298             # normalize multiple single quotes
1299             $value = preg_replace(qr/"{2,}/m, '"', $value);
1300              
1301             # normalize quoted numerical values and asterisks
1302             $value = preg_replace(qr/"(\d+)"/m, '$1', $value);
1303              
1304             # normalize pipe separated request parameters
1305             $value = preg_replace(qr/\|(\w+=\w+)/m, '&$1', $value);
1306              
1307             # normalize ampersand listings
1308             $value = preg_replace(qr/(\w\s)&\s(\w)/, '$1$2', $value);
1309              
1310             return $value;
1311             }
1312              
1313             #****if* IDS/_run_centrifuge
1314             # NAME
1315             # _run_centrifuge
1316             # DESCRIPTION
1317             # The centrifuge prototype
1318             # INPUT
1319             # value the string to convert
1320             # OUTPUT
1321             # value converted string
1322             # SYNOPSIS
1323             # IDS::_run_centrifuge($value);
1324             #****
1325              
1326             sub _run_centrifuge {
1327             my ($value) = @_;
1328              
1329             my $threshold = 3.49;
1330              
1331             if (strlen($value) > 25) {
1332             # strip padding
1333             my $tmp_value = preg_replace(qr/\s{4}|==$/m, '', $value);
1334             $tmp_value = preg_replace(
1335             qr/\s{4}|[\p{L}\d\+\-=,.%()]{8,}/m,
1336             'aaa',
1337             $tmp_value
1338             );
1339              
1340             # Check for the attack char ratio
1341             $tmp_value = preg_replace(qr/([*.!?+-])\1{1,}/m, '$1', $tmp_value);
1342             $tmp_value = preg_replace(qr/"[\p{L}\d\s]+"/m, '', $tmp_value);
1343              
1344             my $stripped_length = strlen(
1345             preg_replace(qr/[\d\s\p{L}\.:,%&\/><\-)!]+/m,
1346             '',
1347             $tmp_value)
1348             );
1349             my $overall_length = strlen(
1350             preg_replace(
1351             qr/([\d\s\p{L}:,\.]{3,})+/m,
1352             'aaa',
1353             preg_replace(
1354             qr/\s{2,}/ms,
1355             '',
1356             $tmp_value
1357             )
1358             )
1359             );
1360              
1361             if ($stripped_length != 0 &&
1362             $overall_length/$stripped_length <= $threshold
1363             ) {
1364             $value .= "\n".'$[!!!]';
1365             }
1366             }
1367              
1368             if (strlen($value) > 40) {
1369             # Replace all non-special chars
1370             my $converted = preg_replace(qr/[\w\s\p{L},.:!]/, '', $value);
1371              
1372             # Split string into an array, unify and sort
1373             my @array = str_split($converted);
1374             my %seen = ();
1375             my @unique = grep { ! $seen{$_} ++ } @array;
1376             @unique = sort @unique;
1377              
1378             # Normalize certain tokens
1379             my %schemes = (
1380             '~' => '+',
1381             '^' => '+',
1382             '|' => '+',
1383             '*' => '+',
1384             '%' => '+',
1385             '&' => '+',
1386             '/' => '+',
1387             );
1388              
1389             $converted = implode('', @unique);
1390             $converted = str_replace([keys %schemes], [values %schemes], $converted);
1391             $converted = preg_replace(qr/[+-]\s*\d+/, '+', $converted);
1392             $converted = preg_replace(qr/[()[\]{}]/, '(', $converted);
1393             $converted = preg_replace(qr/[!?:=]/, ':', $converted);
1394             $converted = preg_replace(qr/[^:(+]/, '', stripslashes($converted)); #/
1395              
1396             # Sort again and implode
1397             @array = str_split($converted);
1398             @array = sort @array;
1399             $converted = implode('', @array);
1400              
1401             if (preg_match(qr/(?:\({2,}\+{2,}:{2,})|(?:\({2,}\+{2,}:+)|(?:\({3,}\++:{2,})/, $converted)) {
1402             return $value . "\n" . $converted;
1403             }
1404             }
1405              
1406             return $value;
1407             }
1408              
1409             #------------------------- PHP functions ---------------------------------------
1410              
1411             #****if* IDS/array_sum
1412             # NAME
1413             # array_sum
1414             # DESCRIPTION
1415             # Equivalent to PHP's array_sum, sums all array values
1416             # INPUT
1417             # array the string to convert
1418             # OUTPUT
1419             # sum sum of all array values
1420             # SYNOPSIS
1421             # IDS::array_sum(@array);
1422             #****
1423              
1424             sub array_sum {
1425             (my @array) = @_;
1426              
1427             my $sum = 0;
1428             foreach my $value (@array) {
1429             if ($value) {
1430             $sum += $value;
1431             }
1432             }
1433             return $sum;
1434             }
1435              
1436             #****if* IDS/preg_match
1437             # NAME
1438             # preg_match
1439             # DESCRIPTION
1440             # Equivalent to PHP's preg_match, but with two arguments only
1441             # INPUT
1442             # pattern the pattern to match
1443             # string the string
1444             # OUTPUT
1445             # boolean 1 if pattern matches string, 0 otherwise
1446             # SYNOPSIS
1447             # IDS::preg_match($pattern, $string);
1448             #****
1449              
1450             sub preg_match {
1451             (my $pattern, my $string) = @_;
1452             return ($string =~ $pattern);
1453             }
1454              
1455             #****if* IDS/preg_match_all
1456             # NAME
1457             # preg_match_all
1458             # DESCRIPTION
1459             # Equivalent to PHP's preg_match_all, but with three arguments only.
1460             # Does not return nested arrays like PHP.
1461             # Does not automatically match entire RegEx in $matches[0] like PHP does -
1462             # Use brackets around your entire RegEx instead: preg_match_all(qr/(your(\d)(R|r)egex)/.
1463             # INPUT
1464             # pattern the pattern to match
1465             # string the string
1466             # arrayref the array to store the matches in
1467             # OUTPUT
1468             # array same content as written into arrayref
1469             # SYNOPSIS
1470             # IDS::preg_match_all(qr/(?:[\d+-=\/\* ]+(?:\s?,\s?[\d+-=\/\* ]+)+){4,}/ms, $value, \@matches)
1471             # if (IDS::preg_match_all(qr/(?:[\d+-=\/\* ]+(?:\s?,\s?[\d+-=\/\* ]+)+){4,}/ms, $value, \@matches)) {
1472             # print 'match';
1473             # }
1474             #****
1475              
1476             sub preg_match_all {
1477             (my $pattern, my $string, my $matches) = @_;
1478             return (@$matches = ($string =~ /$pattern/g));
1479             }
1480              
1481             #****if* IDS/preg_replace
1482             # NAME
1483             # preg_replace
1484             # DESCRIPTION
1485             # Equivalent to PHP's preg_replace, but with three arguments only
1486             # INPUT
1487             # + pattern the pattern(s) to match
1488             # replacement the replacement(s)
1489             # + string the string(s)
1490             # OUTPUT
1491             # string the string(s) with all replacements done
1492             # SYNOPSIS
1493             # IDS::preg_replace(\@patterns, $replacement, $string);
1494             # IDS::preg_replace(qr/^f.*ck/i, 'censored', $string);
1495             # IDS::preg_replace(['badword', 'badword2', 'badword3'], ['censored1', 'censored2', 'censored3'], $string);
1496             #****
1497              
1498             sub preg_replace {
1499             (my $patterns, my $replacements, my $strings) = @_;
1500              
1501             # check input
1502             if (!defined($strings) || !$strings ||
1503             !defined($patterns) || !$patterns ) {
1504             return '';
1505             }
1506              
1507             my $return_string = '';
1508             if (ref($strings) ne 'ARRAY') {
1509             $return_string = $strings;
1510             }
1511              
1512             if (ref($strings) eq 'ARRAY') {
1513             my @replaced_strings = map {
1514             preg_replace($patterns, $replacements, $_);
1515             } @$strings;
1516             return \@replaced_strings;
1517             }
1518             elsif (ref($patterns) eq 'ARRAY') {
1519             my $pattern_no = 0;
1520             foreach my $pattern (@$patterns) {
1521             if (ref($replacements) eq 'ARRAY') {
1522             $return_string = preg_replace($pattern, @$replacements[$pattern_no++], $return_string);
1523             }
1524             else {
1525             $return_string = preg_replace($pattern, $replacements, $return_string);
1526             }
1527             }
1528             }
1529             else {
1530             my $repl = '';
1531              
1532             if (ref($replacements) eq 'ARRAY') {
1533             $repl = @$replacements[0];
1534             }
1535             else {
1536             if (!defined($replacements)) {
1537             $repl = '';
1538             }
1539             else {
1540             $repl = $replacements;
1541             }
1542             }
1543             $repl =~ s/\\/\\\\/g;
1544             $repl =~ s/\"/\\"/g;
1545             $repl =~ s/\@/\\@/g;
1546             $repl =~ s/\$(?!\d)/\\\$/g; # escape $ if not substitution variable like $1
1547             $repl = qq{"$repl"};
1548             $return_string =~ s/$patterns/defined $repl ? $repl : ''/eeg;
1549             }
1550             return $return_string;
1551             }
1552              
1553             #****if* IDS/str_replace
1554             # NAME
1555             # str_replace
1556             # DESCRIPTION
1557             # Equivalent to PHP's str_replace, but with three arguments only (simply a wrapper for preg_replace, but escapes pattern meta characters)
1558             # INPUT
1559             # pattern the pattern(s) to match
1560             # replacement the replacement(s)
1561             # string the string(s)
1562             # OUTPUT
1563             # string the string(s) with all replacements done
1564             # SYNOPSIS
1565             # IDS::str_replace(\@patterns, $replacement, $string);
1566             # IDS::str_replace('bad\tword', 'censored', $string); # replaces 'bad\tword' but not 'bad word' or "bad\tword"
1567             # IDS::str_replace(['badword', 'badword2', 'badword3'], ['censored1', 'censored2', 'censored3'], $string);
1568             #****
1569              
1570             sub str_replace {
1571             (my $patterns, my $replacements, my $strings) = @_;
1572              
1573             my @escapedpatterns = ();
1574              
1575             if (ref($patterns) eq 'ARRAY') {
1576             @escapedpatterns = map {quotemeta($_)} @$patterns;
1577             return preg_replace(\@escapedpatterns, $replacements, $strings);
1578             }
1579             else {
1580             return preg_replace(quotemeta($patterns), $replacements, $strings);
1581             }
1582             }
1583              
1584             #****if* IDS/str_split
1585             # NAME
1586             # str_split
1587             # DESCRIPTION
1588             # Equivalent to PHP's str_split
1589             # INPUT
1590             # string the string to split
1591             # OUTPUT
1592             # array the split string
1593             # SYNOPSIS
1594             # IDS::str_split($string);
1595             #****
1596              
1597             sub str_split {
1598             (my $string, my $limit) = @_;
1599             if (defined($limit)) {
1600             return ($string =~ /(.{1,$limit})/g);
1601             }
1602             else {
1603             return split(//, $string);
1604             }
1605             }
1606              
1607             #****if* IDS/strlen
1608             # NAME
1609             # strlen
1610             # DESCRIPTION
1611             # Equivalent to PHP's strlen, wrapper for Perl's length()
1612             # INPUT
1613             # string the string
1614             # OUTPUT
1615             # string the string's length
1616             # SYNOPSIS
1617             # IDS::strlen($url);
1618             #****
1619              
1620             sub strlen {
1621             (my $string) = @_;
1622             return length($string);
1623             }
1624              
1625             #****if* IDS/urldecode
1626             # NAME
1627             # urldecode
1628             # DESCRIPTION
1629             # Equivalent to PHP's urldecode
1630             # INPUT
1631             # string the URL to decode
1632             # OUTPUT
1633             # string the decoded URL
1634             # SYNOPSIS
1635             # IDS::urldecode($url);
1636             #****
1637              
1638             sub urldecode {
1639             (my $theURL) = @_;
1640             $theURL =~ tr/+/ /;
1641             $theURL =~ s/%([a-fA-F0-9]{2,2})/chr(hex($1))/eg;
1642             $theURL =~ s///g;
1643             utf8::decode($theURL);
1644             return $theURL;
1645             }
1646              
1647             #****if* IDS/urlencode
1648             # NAME
1649             # urlencode
1650             # DESCRIPTION
1651             # Equivalent to PHP's urlencode
1652             # INPUT
1653             # string the URL to encode
1654             # OUTPUT
1655             # string the encoded URL
1656             # SYNOPSIS
1657             # IDS::urlencode($url);
1658             #****
1659              
1660             sub urlencode {
1661             (my $theURL) = @_;
1662             $theURL =~ s/([\W])/sprintf("%%%02X",ord($1))/eg;
1663             utf8::encode($theURL);
1664             return $theURL;
1665             }
1666              
1667             #****if* IDS/implode
1668             # NAME
1669             # implode
1670             # DESCRIPTION
1671             # Equivalent to PHP's implode (simply wrapper of join)
1672             # INPUT
1673             # string glue the glue to put between the pieces
1674             # array pieces the pieces to be put together
1675             # OUTPUT
1676             # string the imploded string
1677             # SYNOPSIS
1678             # IDS::implode(';', @pieces);
1679             #****
1680              
1681             sub implode {
1682             (my $glue, my @pieces) = @_;
1683             return join($glue, @pieces);
1684             }
1685              
1686             #****if* IDS/explode
1687             # NAME
1688             # explode
1689             # DESCRIPTION
1690             # Equivalent to PHP's explode (simply wrapper of split, but escapes met characters)
1691             # INPUT
1692             # string glue the glue to put between the pieces
1693             # string string the string to split
1694             # OUTPUT
1695             # array the exploded string
1696             # SYNOPSIS
1697             # IDS::explode(';', $string);
1698             #****
1699              
1700             sub explode {
1701             (my $glue, my $string) = @_;
1702             return split(quotemeta($glue), $string);
1703             }
1704              
1705             #****if* IDS/stripslashes
1706             # NAME
1707             # stripslashes
1708             # DESCRIPTION
1709             # Equivalent to PHP's stripslashes
1710             # INPUT
1711             # string string the string
1712             # OUTPUT
1713             # string the stripped string
1714             # SYNOPSIS
1715             # IDS::stripslashes($string);
1716             #****
1717              
1718             sub stripslashes {
1719             (my $string) = @_;
1720             # $string =~ s/(?:\\(\'|\"|\\|\0|N))/$1/g;
1721             $string =~ s/\\([^\\])/$1/g;
1722             return $string;
1723             }
1724              
1725             #****if* IDS/strip_tags
1726             # NAME
1727             # strip_tags
1728             # DESCRIPTION
1729             # Equivalent to PHP's strip_tags, but without 'allowable_tags' parameter
1730             # INPUT
1731             # string string the string
1732             # OUTPUT
1733             # string the stripped string
1734             # SYNOPSIS
1735             # IDS::strip_tags($string);
1736             #****
1737              
1738             sub strip_tags {
1739             (my $string) = @_;
1740              
1741             while ($string =~ s/<\S[^<>]*(?:>|$)//gs) {};
1742              
1743             return $string;
1744             }
1745              
1746             1;
1747              
1748             __END__