From 3f80fdac3b2f7e4f10323ed70118f4b05e6682a0 Mon Sep 17 00:00:00 2001 From: ivanr Date: Tue, 6 Feb 2007 12:29:22 +0000 Subject: [PATCH] Import ModSecurity 2.1.0-rc7 --- CHANGES | 144 + LICENSE | 340 ++ README.TXT | 8 + apache2/.deps | 0 apache2/LICENSE | 340 ++ apache2/Makefile | 40 + apache2/Makefile.win | 42 + apache2/apache2.h | 70 + apache2/apache2_config.c | 1326 +++++++ apache2/apache2_io.c | 588 +++ apache2/apache2_util.c | 322 ++ apache2/api/README | 26 + apache2/api/mod_op_strstr.c | 176 + apache2/api/mod_tfn_reverse.c | 86 + apache2/mod_security2.c | 991 ++++++ apache2/modsecurity.c | 468 +++ apache2/modsecurity.h | 469 +++ apache2/modules.mk | 19 + apache2/msc_logging.c | 890 +++++ apache2/msc_logging.h | 46 + apache2/msc_multipart.c | 856 +++++ apache2/msc_multipart.h | 120 + apache2/msc_parsers.c | 305 ++ apache2/msc_parsers.h | 25 + apache2/msc_pcre.c | 93 + apache2/msc_pcre.h | 39 + apache2/msc_reqbody.c | 636 ++++ apache2/msc_util.c | 981 +++++ apache2/msc_util.h | 75 + apache2/msc_xml.c | 137 + apache2/msc_xml.h | 43 + apache2/persist_dbm.c | 514 +++ apache2/persist_dbm.h | 26 + apache2/re.c | 1479 ++++++++ apache2/re.h | 299 ++ apache2/re_actions.c | 1796 ++++++++++ apache2/re_operators.c | 986 ++++++ apache2/re_tfns.c | 512 +++ apache2/re_variables.c | 2443 +++++++++++++ doc/apache_request_cycle-modsecurity.jpg | Bin 0 -> 92271 bytes doc/breach-logo-small.gif | Bin 0 -> 2389 bytes doc/html-chunked.xsl | 292 ++ doc/html.xsl | 20 + doc/index.html | 35 + doc/modsecurity-logo.png | Bin 0 -> 100823 bytes doc/modsecurity-reference.css | 102 + doc/modsecurity.gif | Bin 0 -> 2585 bytes doc/modsecurity2-apache-reference.xml | 4134 ++++++++++++++++++++++ doc/pdf.xsl | 165 + modsecurity.conf-minimal | 32 + 50 files changed, 22536 insertions(+) create mode 100644 CHANGES create mode 100644 LICENSE create mode 100644 README.TXT create mode 100644 apache2/.deps create mode 100644 apache2/LICENSE create mode 100644 apache2/Makefile create mode 100644 apache2/Makefile.win create mode 100644 apache2/apache2.h create mode 100644 apache2/apache2_config.c create mode 100644 apache2/apache2_io.c create mode 100644 apache2/apache2_util.c create mode 100644 apache2/api/README create mode 100644 apache2/api/mod_op_strstr.c create mode 100644 apache2/api/mod_tfn_reverse.c create mode 100644 apache2/mod_security2.c create mode 100644 apache2/modsecurity.c create mode 100644 apache2/modsecurity.h create mode 100644 apache2/modules.mk create mode 100644 apache2/msc_logging.c create mode 100644 apache2/msc_logging.h create mode 100644 apache2/msc_multipart.c create mode 100644 apache2/msc_multipart.h create mode 100644 apache2/msc_parsers.c create mode 100644 apache2/msc_parsers.h create mode 100644 apache2/msc_pcre.c create mode 100644 apache2/msc_pcre.h create mode 100644 apache2/msc_reqbody.c create mode 100644 apache2/msc_util.c create mode 100644 apache2/msc_util.h create mode 100644 apache2/msc_xml.c create mode 100644 apache2/msc_xml.h create mode 100644 apache2/persist_dbm.c create mode 100644 apache2/persist_dbm.h create mode 100644 apache2/re.c create mode 100644 apache2/re.h create mode 100644 apache2/re_actions.c create mode 100644 apache2/re_operators.c create mode 100644 apache2/re_tfns.c create mode 100644 apache2/re_variables.c create mode 100644 doc/apache_request_cycle-modsecurity.jpg create mode 100644 doc/breach-logo-small.gif create mode 100644 doc/html-chunked.xsl create mode 100644 doc/html.xsl create mode 100644 doc/index.html create mode 100644 doc/modsecurity-logo.png create mode 100644 doc/modsecurity-reference.css create mode 100644 doc/modsecurity.gif create mode 100644 doc/modsecurity2-apache-reference.xml create mode 100644 doc/pdf.xsl create mode 100644 modsecurity.conf-minimal diff --git a/CHANGES b/CHANGES new file mode 100644 index 00000000..7f7e3e1f --- /dev/null +++ b/CHANGES @@ -0,0 +1,144 @@ + +05 Feb 2006 - 2.1.0-rc7 +------------------------ + +* Fixed a problem with incorrectly setting requestBodyProcessor using + the ctl action. + + +24 Jan 2007 - 2.1.0-rc6 +----------------------- + +* Bundled Core Rules 2.1-1.3.2b4. + +* Updates to the reference manual. + +* Reversed the return values of @validateDTD and @validateSchema, to + make them consistent with other operators. + +* Added a few helpful debug messages in the XML validation area. + + +05 Jan 2007 - 2.1.0-rc5 +----------------------- + +* Updates to the reference manual. + + +29 Dec 2006 - 2.1.0-rc4 +----------------------- + +* Fixed the validateByteRange operator. + +* Default value for the status action is now 403 (as it was supposed to + be but it was effectively 500). + +* Rule exceptions (removing using an ID range or an regular expression) + is now applied to the current context too. (Previously it only worked + on rules that are inherited from the parent context.) + +* Fix of a bug with expired variables. + +* Fixed regular expression variable selectors for many collections. + +* Performance improvements - up to two times for real-life work loads! + +* Memory consumption improvements (not measured but significant). + +* The allow action did not work in phases 3 and 4. Fixed. + +* Unlocked collections GLOBAL and RESOURCE. + +* Added support for variable expansion in the msg action. + +* New feature: It is now possible to make relative changes to the + audit log parts with the ctl action. For example: "ctl:auditLogParts=+E". + +* New feature: "tag" action. To be used for event categorisation. + +* XML parser was not reporting errors that occured at the end + of XML payload. + +* Files were not extracted from request if SecUploadKeepFiles was + Off. Fixed. + +* Regular expressions that are too long are truncated to 256 + characters before used in error messages. (In order to keep + the error messages in the log at a reasonable size.) + +* Fixed the sha1 transformation function. + +* Fixed the skip action. + +* Fixed REQUEST_PROTOCOL, REMOTE_USER, and AUTH_TYPE. + +* SecRuleEngine did not work in child configuration contexts + (e.g. ). + +* Fixed base64Decode and base64Encode. + + +15 Nov 2006 - 2.0.4 +------------------- + +* Fixed the "deprecatevar" action. + +* Decreasing variable values did not work. + +* Made "nolog" do what it is supposed to do - cause a rule match to + not be logged. Also "nolog" now implies "noauditlog" but it's + possible to follow "nolog" with "auditlog" and have the match + not logged to the error log but logged to the auditlog. (Not + something that strikes me as useful but it's possible.) + +* Relative paths given to SecDataDir will now be treated as relative + to the Apache server root. + +* Added checks to make sure only correct actions are specified in + SecDefaultAction (some actions are required, some don't make any + sense) and in rules that are not chain starters (same). This should + make the unhelpful "Internal Error: Failed to add rule to the ruleset" + message go away. + +* Fixed the problem when "SecRuleInheritance Off" is used in a context + with no rules defined. + +* Fixed a problem of lost input (request body) data on some redirections, + for example when mod_rewrite is used. + + +26 Oct 2006 - 2.0.3 +------------------- + +* Fixed a memory leak (all platforms) and a concurrency control + problem that could cause a crash (multithreaded platforms only). + +* Fixed a SecAuditLogRelevantStatus problem, which would not work + properly unless the regular expression contained a subexpression. + + +19 Oct 2006 - 2.0.2 +------------------- + +* Fixed incorrect permissions on the global mutex, which prevented + the mutex from working properly. + +* Fixed incorrect actionset merging where the status was copied from + the child actionset even though it was not defined. + +* Fixed missing metadata information (in the logs) for warnings. + + +16 Oct 2006 - 2.0.1 +------------------- + +* Rules that used operator negation did not work. Fixed. + +* Fixed bug that prevented invalid regular expressions from being reported. + + +16 Oct 2006 - 2.0.0 +------------------- + +* First stable 2.x release. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..eeb586b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README.TXT b/README.TXT new file mode 100644 index 00000000..666c25a0 --- /dev/null +++ b/README.TXT @@ -0,0 +1,8 @@ + +ModSecurity for Apache (http://www.modsecurity.org) +Copyright (C) 2004-2006 Breach Security, Inc. (http://www.breach.com) + +DOCUMENTATION + +Please refer to the documentation folder (/doc) for +the reference manual. diff --git a/apache2/.deps b/apache2/.deps new file mode 100644 index 00000000..e69de29b diff --git a/apache2/LICENSE b/apache2/LICENSE new file mode 100644 index 00000000..eeb586b3 --- /dev/null +++ b/apache2/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) 19yy + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) 19yy name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/apache2/Makefile b/apache2/Makefile new file mode 100644 index 00000000..5373a2d7 --- /dev/null +++ b/apache2/Makefile @@ -0,0 +1,40 @@ +builddir = . + +# To successfully build ModSecurity for Apache you will +# need to configure the "top_dir" variable properly. The +# correct value will differ from system to system. +# +# If you've installed Apache manually simply point to the +# installation directory. Most pre-packaged installations +# consist of two parts. One contains the binaries, and the +# other contains the "development" files. You will typically +# need both. +# +# The list below may help: +# +# Fedora Core - /usr/lib/httpd (the httpd-devel package must be installed) +# +# Debian - /usr/share/apache2 (apache2-prefork-dev or apache2-threaded-dev +# needed, depending on your installation type) +# +top_dir = /home/ivanr/apache22 + +top_srcdir = ${top_dir} +top_builddir = ${top_dir} + +include ${top_builddir}/build/special.mk + +APXS = apxs +APACHECTL = apachectl + +INCLUDES = -I /usr/include/libxml2 +DEFS = -DWITH_LIBXML2 +#LIBS = -Lmy/lib/dir -lmylib + +CFLAGS = -O2 -g -Wuninitialized -Wall -Wmissing-prototypes -Wshadow -Wunused-variable -Wunused-value -Wchar-subscripts -Wsign-compare + +all: local-shared-build + +clean: + -rm -f *.o *.lo *.slo *.la *~ .libs + \ No newline at end of file diff --git a/apache2/Makefile.win b/apache2/Makefile.win new file mode 100644 index 00000000..59dd226c --- /dev/null +++ b/apache2/Makefile.win @@ -0,0 +1,42 @@ + +# Path to Apache installation +BASE = "C:/Program Files/Apache Group/Apache2/" + +CC = cl + +# Add -DWITH_LIBXML2 below if you want to link against libxml2 +DEFS = /nologo /Od /LD /W3 -DWIN32 -DWINNT +DLL = mod_security2.dll + +# Path to the headers - configure the libxml2 include path +INCLUDES = -I. -I$(BASE)\include -IC:\libxml2\include + +CFLAGS= -O $(INCLUDES) $(DEFS) + +# Paths to the required libraries +# Use the line below if you want to link against libxml2 +# LIBS = $(BASE)\lib\libhttpd.lib $(BASE)\lib\libapr.lib $(BASE)\lib\libaprutil.lib $(BASE)\lib\pcre.lib C:\libxml2\lib\libxml2.lib + +# Use the line belof if you don't want to link against libxml2 +LIBS = $(BASE)\lib\libhttpd.lib $(BASE)\lib\libapr.lib $(BASE)\lib\libaprutil.lib $(BASE)\lib\pcre.lib + +OBJS = mod_security2.obj apache2_config.obj apache2_io.obj apache2_util.obj \ + re.obj re_operators.obj re_actions.obj re_tfns.obj re_variables.obj \ + msc_logging.obj msc_xml.obj msc_multipart.obj modsecurity.obj msc_parsers.obj \ + msc_util.obj msc_pcre.obj persist_dbm.obj msc_reqbody.obj + +all: $(DLL) + +dll: $(DLL) + +.c.obj: + $(CC) $(CFLAGS) -c $< -Fo$@ + +.cpp.obj: + $(CC) $(CFLAGS) -c $< -Fo$@ + +$(DLL): $(OBJS) + $(CC) $(CFLAGS) -LD $(OBJS) -Fe$(DLL) $(LIBS) /link /NODEFAULTLIB:MSVCRT + +clean: + del $(OBJS) *.dll *.lib *.pdb *.idb *.ilk *.exp *.res *.rc *.bin diff --git a/apache2/apache2.h b/apache2/apache2.h new file mode 100644 index 00000000..d20fe0d8 --- /dev/null +++ b/apache2/apache2.h @@ -0,0 +1,70 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: apache2.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _APACHE2_H_ +#define _APACHE2_H_ + +#include "http_core.h" +#include "http_request.h" + +#include +#include + + +/* Optional functions. */ + +APR_DECLARE_OPTIONAL_FN(void, modsec_register_tfn, (const char *name, void *fn)); +APR_DECLARE_OPTIONAL_FN(void, modsec_register_operator, (const char *name, void *fn_init, void *fn_exec)); + + +/* Configuration functions. */ + +void DSOLOCAL *create_directory_config(apr_pool_t *mp, char *path); + +void DSOLOCAL *merge_directory_configs(apr_pool_t *mp, void *_parent, void *_child); + +void DSOLOCAL init_directory_config(directory_config *dcfg); + + +/* IO functions. */ + +apr_status_t DSOLOCAL input_filter(ap_filter_t *f, apr_bucket_brigade *bb_out, + ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes); + +apr_status_t DSOLOCAL output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in); + +apr_status_t DSOLOCAL read_request_body(modsec_rec *msr, char **error_msg); + + +/* Utility functions */ + +int DSOLOCAL perform_interception(modsec_rec *msr); + +int DSOLOCAL apache2_exec(modsec_rec *msr, const char *command, const char **argv, char **output); + +void DSOLOCAL record_time_checkpoint(modsec_rec *msr, int checkpoint_no); + +char DSOLOCAL *get_apr_error(apr_pool_t *p, apr_status_t rc); + +char DSOLOCAL *get_env_var(request_rec *r, char *name); + +void DSOLOCAL internal_log(request_rec *r, directory_config *dcfg, modsec_rec *msr, + int level, const char *text, va_list ap); + +void DSOLOCAL msr_log(modsec_rec *msr, int level, const char *text, ...); + +char DSOLOCAL *format_error_log_message(apr_pool_t *mp, error_message *em); + +const DSOLOCAL char *get_response_protocol(request_rec *r); + +#endif + diff --git a/apache2/apache2_config.c b/apache2/apache2_config.c new file mode 100644 index 00000000..a69a62da --- /dev/null +++ b/apache2/apache2_config.c @@ -0,0 +1,1326 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: apache2_config.c,v 1.8 2006/12/28 10:39:13 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "modsecurity.h" +#include "msc_logging.h" + +#include "http_log.h" + +/* #define DEBUG_CONF 1 */ + +/* -- Directory context creation and initialisation -- */ + +/** + * Creates a fresh directory configuration. + */ +void *create_directory_config(apr_pool_t *mp, char *path) { + directory_config *dcfg = (directory_config *)apr_pcalloc(mp, sizeof(directory_config)); + if (dcfg == NULL) return NULL; + + #ifdef DEBUG_CONF + fprintf(stderr, "Created directory config %x path %s\n", dcfg, path); + #endif + + dcfg->mp = mp; + dcfg->is_enabled = NOT_SET; + + dcfg->reqbody_access = NOT_SET; + dcfg->reqbody_inmemory_limit = NOT_SET; + dcfg->reqbody_limit = NOT_SET; + dcfg->resbody_access = NOT_SET; + + dcfg->debuglog_name = NOT_SET_P; + dcfg->debuglog_level = NOT_SET; + dcfg->debuglog_fd = NOT_SET_P; + + dcfg->of_limit = NOT_SET; + dcfg->of_mime_types = NOT_SET_P; + dcfg->of_mime_types_cleared = NOT_SET; + + dcfg->cookie_format = NOT_SET; + dcfg->argument_separator = NOT_SET; + + dcfg->rule_inheritance = NOT_SET; + dcfg->rule_exceptions = apr_array_make(mp, 16, sizeof(rule_exception *)); + + /* audit log variables */ + dcfg->auditlog_flag = NOT_SET; + dcfg->auditlog_type = NOT_SET; + dcfg->auditlog_name = NOT_SET_P; + dcfg->auditlog_fd = NOT_SET_P; + dcfg->auditlog_storage_dir = NOT_SET_P; + dcfg->auditlog_parts = NOT_SET_P; + dcfg->auditlog_relevant_regex = NOT_SET_P; + + dcfg->ruleset = NULL; + + /* Upload */ + dcfg->tmp_dir = NOT_SET_P; + dcfg->upload_dir = NOT_SET_P; + dcfg->upload_keep_files = NOT_SET; + dcfg->upload_validates_files = NOT_SET; + + /* These are only used during the configuration process. */ + dcfg->tmp_chain_starter = NULL; + dcfg->tmp_default_actionset = NULL; + + /* Misc */ + dcfg->data_dir = NOT_SET_P; + dcfg->webappid = NOT_SET_P; + + return dcfg; +} + +/** + * Copies rules between one phase of two configuration contexts, + * taking exceptions into account. + */ +static void copy_rules_phase(apr_pool_t *mp, apr_array_header_t *parent_phase_arr, + apr_array_header_t *child_phase_arr, apr_array_header_t *exceptions_arr) +{ + rule_exception **exceptions; + msre_rule **rules; + int i, j; + int mode = 0; + + rules = (msre_rule **)parent_phase_arr->elts; + for(i = 0; i < parent_phase_arr->nelts; i++) { + msre_rule *rule = (msre_rule *)rules[i]; + int copy = 1; + + if (mode == 0) { + /* First rule in the chain. */ + exceptions = (rule_exception **)exceptions_arr->elts; + for(j = 0; j < exceptions_arr->nelts; j++) { + + /* Process exceptions. */ + switch(exceptions[j]->type) { + case RULE_EXCEPTION_REMOVE_ID : + if ((rule->actionset != NULL)&&(rule->actionset->id != NULL)) { + int ruleid = atoi(rule->actionset->id); + if (rule_id_in_range(ruleid, exceptions[j]->param)) copy--; + } + break; + case RULE_EXCEPTION_REMOVE_MSG : + if ((rule->actionset != NULL)&&(rule->actionset->msg != NULL)) { + char *my_error_msg = NULL; + + int rc = msc_regexec(exceptions[j]->param_data, + rule->actionset->msg, strlen(rule->actionset->msg), + &my_error_msg); + if (rc >= 0) copy--; + } + break; + } + } + + if (copy > 0) { + /* Copy the rule. */ + *(msre_rule **)apr_array_push(child_phase_arr) = rule; + if (rule->actionset->is_chained) mode = 2; + } else { + if (rule->actionset->is_chained) mode = 1; + } + } else { + if (mode == 2) { + /* Copy the rule (it belongs to the chain we want to include. */ + *(msre_rule **)apr_array_push(child_phase_arr) = rule; + } + + if ((rule->actionset == NULL)||(rule->actionset->is_chained == 0)) mode = 0; + } + } +} + +/** + * Copies rules between two configuration contexts, + * taking exceptions into account. + */ +static int copy_rules(apr_pool_t *mp, msre_ruleset *parent_ruleset, msre_ruleset *child_ruleset, + apr_array_header_t *exceptions_arr) +{ + copy_rules_phase(mp, parent_ruleset->phase_request_headers, + child_ruleset->phase_request_headers, exceptions_arr); + copy_rules_phase(mp, parent_ruleset->phase_request_body, + child_ruleset->phase_request_body, exceptions_arr); + copy_rules_phase(mp, parent_ruleset->phase_response_headers, + child_ruleset->phase_response_headers, exceptions_arr); + copy_rules_phase(mp, parent_ruleset->phase_response_body, + child_ruleset->phase_response_body, exceptions_arr); + copy_rules_phase(mp, parent_ruleset->phase_logging, + child_ruleset->phase_logging, exceptions_arr); + + return 1; +} + +/** + * Merges two directory configurations. + */ +void *merge_directory_configs(apr_pool_t *mp, void *_parent, void *_child) { + directory_config *parent = (directory_config *)_parent; + directory_config *child = (directory_config *)_child; + directory_config *merged = create_directory_config(mp, NULL); + + #ifdef DEBUG_CONF + fprintf(stderr, "Merge parent %x child %x RESULT %x\n", _parent, _child, merged); + #endif + + if (merged == NULL) return NULL; + + /* Use values from the child configuration where possible, + * otherwise use the parent's. + */ + + merged->is_enabled = (child->is_enabled == NOT_SET + ? parent->is_enabled : child->is_enabled); + + /* IO parameters */ + merged->reqbody_access = (child->reqbody_access == NOT_SET + ? parent->reqbody_access : child->reqbody_access); + merged->reqbody_inmemory_limit = (child->reqbody_inmemory_limit == NOT_SET + ? parent->reqbody_inmemory_limit : child->reqbody_inmemory_limit); + merged->reqbody_limit = (child->reqbody_limit == NOT_SET + ? parent->reqbody_limit : child->reqbody_limit); + merged->resbody_access = (child->resbody_access == NOT_SET + ? parent->resbody_access : child->resbody_access); + + merged->of_limit = (child->of_limit == NOT_SET + ? parent->of_limit : child->of_limit); + + if (child->of_mime_types != NOT_SET_P) { + /* Child added to the table */ + + if (child->of_mime_types_cleared == 1) { + /* The list of MIME types was cleared in the child, + * which means the parent's MIME types went away and + * we should not take them into consideration here. + */ + merged->of_mime_types = child->of_mime_types; + merged->of_mime_types_cleared = 1; + } else { + /* Add MIME types defined in the child to those + * defined in the parent context. + */ + if (parent->of_mime_types == NOT_SET_P) { + merged->of_mime_types = child->of_mime_types; + merged->of_mime_types_cleared = NOT_SET; + } else { + merged->of_mime_types = apr_table_overlay(mp, parent->of_mime_types, + child->of_mime_types); + if (merged->of_mime_types == NULL) return NULL; + } + } + } else { + /* Child did not add to the table */ + + if (child->of_mime_types_cleared == 1) { + merged->of_mime_types_cleared = 1; + } else { + merged->of_mime_types = parent->of_mime_types; + merged->of_mime_types_cleared = parent->of_mime_types_cleared; + } + } + + /* debug log */ + if (child->debuglog_fd == NOT_SET_P) { + merged->debuglog_name = parent->debuglog_name; + merged->debuglog_fd = parent->debuglog_fd; + } else { + merged->debuglog_name = child->debuglog_name; + merged->debuglog_fd = child->debuglog_fd; + } + + merged->debuglog_level = (child->debuglog_level == NOT_SET + ? parent->debuglog_level : child->debuglog_level); + + merged->cookie_format = (child->cookie_format == NOT_SET + ? parent->cookie_format : child->cookie_format); + merged->argument_separator = (child->argument_separator == NOT_SET + ? parent->argument_separator : child->argument_separator); + + + /* rule inheritance */ + if ((child->rule_inheritance == NOT_SET)||(child->rule_inheritance == 1)) { + merged->rule_inheritance = parent->rule_inheritance; + if ((child->ruleset == NULL)&&(parent->ruleset == NULL)) { + /* Do nothing, there are no rules in either context. */ + } else + if (child->ruleset == NULL) { + /* Copy the rules from the parent context. */ + merged->ruleset = msre_ruleset_create(parent->ruleset->engine, mp); + copy_rules(mp, parent->ruleset, merged->ruleset, child->rule_exceptions); + } else + if (parent->ruleset == NULL) { + /* Copy child rules. */ + merged->ruleset = msre_ruleset_create(child->ruleset->engine, mp); + merged->ruleset->phase_request_headers = apr_array_copy(mp, + child->ruleset->phase_request_headers); + merged->ruleset->phase_request_body = apr_array_copy(mp, + child->ruleset->phase_request_body); + merged->ruleset->phase_response_headers = apr_array_copy(mp, + child->ruleset->phase_response_headers); + merged->ruleset->phase_response_body = apr_array_copy(mp, + child->ruleset->phase_response_body); + merged->ruleset->phase_logging = apr_array_copy(mp, + child->ruleset->phase_logging); + } else { + /* Copy parent rules, then add child rules to it. */ + merged->ruleset = msre_ruleset_create(parent->ruleset->engine, mp); + copy_rules(mp, parent->ruleset, merged->ruleset, child->rule_exceptions); + + apr_array_cat(merged->ruleset->phase_request_headers, + child->ruleset->phase_request_headers); + apr_array_cat(merged->ruleset->phase_request_body, + child->ruleset->phase_request_body); + apr_array_cat(merged->ruleset->phase_response_headers, + child->ruleset->phase_response_headers); + apr_array_cat(merged->ruleset->phase_response_body, + child->ruleset->phase_response_body); + apr_array_cat(merged->ruleset->phase_logging, + child->ruleset->phase_logging); + } + } else { + merged->rule_inheritance = 0; + if (child->ruleset != NULL) { + /* Copy child rules. */ + merged->ruleset = msre_ruleset_create(child->ruleset->engine, mp); + merged->ruleset->phase_request_headers = apr_array_copy(mp, + child->ruleset->phase_request_headers); + merged->ruleset->phase_request_body = apr_array_copy(mp, + child->ruleset->phase_request_body); + merged->ruleset->phase_response_headers = apr_array_copy(mp, + child->ruleset->phase_response_headers); + merged->ruleset->phase_response_body = apr_array_copy(mp, + child->ruleset->phase_response_body); + merged->ruleset->phase_logging = apr_array_copy(mp, + child->ruleset->phase_logging); + } + } + + /* Merge rule exceptions. */ + merged->rule_exceptions = apr_array_append(mp, parent->rule_exceptions, + child->rule_exceptions); + + /* audit log variables */ + merged->auditlog_flag = (child->auditlog_flag == NOT_SET + ? parent->auditlog_flag : child->auditlog_flag); + merged->auditlog_type = (child->auditlog_type == NOT_SET + ? parent->auditlog_type : child->auditlog_type); + if (child->auditlog_fd != NOT_SET_P) { + merged->auditlog_fd = child->auditlog_fd; + merged->auditlog_name = child->auditlog_name; + } else { + merged->auditlog_fd = parent->auditlog_fd; + merged->auditlog_name = parent->auditlog_name; + } + merged->auditlog_storage_dir = (child->auditlog_storage_dir == NOT_SET_P + ? parent->auditlog_storage_dir : child->auditlog_storage_dir); + merged->auditlog_parts = (child->auditlog_parts == NOT_SET_P + ? parent->auditlog_parts : child->auditlog_parts); + merged->auditlog_relevant_regex = (child->auditlog_relevant_regex == NOT_SET_P + ? parent->auditlog_relevant_regex : child->auditlog_relevant_regex); + + /* Upload */ + merged->tmp_dir = (child->tmp_dir == NOT_SET_P + ? parent->tmp_dir : child->tmp_dir); + merged->upload_dir = (child->upload_dir == NOT_SET_P + ? parent->upload_dir : child->upload_dir); + merged->upload_keep_files = (child->upload_keep_files == NOT_SET + ? parent->upload_keep_files : child->upload_keep_files); + merged->upload_validates_files = (child->upload_validates_files == NOT_SET + ? parent->upload_validates_files : child->upload_validates_files); + + /* Misc */ + merged->data_dir = (child->data_dir == NOT_SET_P + ? parent->data_dir : child->data_dir); + merged->webappid = (child->webappid == NOT_SET_P + ? parent->webappid : child->webappid); + + return merged; +} + +/** + * Initialise directory configuration. This function is *not* meant + * to be called for directory configuration instances created during + * the configuration phase. It can only be called on copies of those + * (created fresh for every transaction). + */ +void init_directory_config(directory_config *dcfg) { + if (dcfg == NULL) return; + + if (dcfg->is_enabled == NOT_SET) dcfg->is_enabled = 0; + + if (dcfg->reqbody_access == NOT_SET) dcfg->reqbody_access = 0; + if (dcfg->reqbody_inmemory_limit == NOT_SET) + dcfg->reqbody_inmemory_limit = REQUEST_BODY_DEFAULT_INMEMORY_LIMIT; + if (dcfg->reqbody_limit == NOT_SET) dcfg->reqbody_limit = REQUEST_BODY_DEFAULT_LIMIT; + if (dcfg->resbody_access == NOT_SET) dcfg->resbody_access = 0; + if (dcfg->of_limit == NOT_SET) dcfg->of_limit = RESPONSE_BODY_DEFAULT_LIMIT; + + if (dcfg->of_mime_types == NOT_SET_P) { + dcfg->of_mime_types = apr_table_make(dcfg->mp, 3); + if (dcfg->of_mime_types_cleared != 1) { + apr_table_setn(dcfg->of_mime_types, "text/plain", "1"); + apr_table_setn(dcfg->of_mime_types, "text/html", "1"); + } + } + + if (dcfg->debuglog_fd == NOT_SET_P) dcfg->debuglog_fd = NULL; + if (dcfg->debuglog_name == NOT_SET_P) dcfg->debuglog_name = NULL; + if (dcfg->debuglog_level == NOT_SET) dcfg->debuglog_level = 0; + + if (dcfg->cookie_format == NOT_SET) dcfg->cookie_format = 0; + if (dcfg->argument_separator == NOT_SET) dcfg->argument_separator = '&'; + + if (dcfg->rule_inheritance == NOT_SET) dcfg->rule_inheritance = 1; + + /* audit log variables */ + if (dcfg->auditlog_flag == NOT_SET) dcfg->auditlog_flag = 0; + if (dcfg->auditlog_type == NOT_SET) dcfg->auditlog_type = AUDITLOG_SERIAL; + if (dcfg->auditlog_fd == NOT_SET_P) dcfg->auditlog_fd = NULL; + if (dcfg->auditlog_name == NOT_SET_P) dcfg->auditlog_name = NULL; + if (dcfg->auditlog_storage_dir == NOT_SET_P) dcfg->auditlog_storage_dir = NULL; + if (dcfg->auditlog_parts == NOT_SET_P) dcfg->auditlog_parts = "ABCFHZ"; + if (dcfg->auditlog_relevant_regex == NOT_SET_P) dcfg->auditlog_relevant_regex = NULL; + + /* Upload */ + if (dcfg->tmp_dir == NOT_SET_P) dcfg->tmp_dir = guess_tmp_dir(dcfg->mp); + if (dcfg->upload_dir == NOT_SET_P) dcfg->upload_dir = NULL; + if (dcfg->upload_keep_files == NOT_SET) dcfg->upload_keep_files = KEEP_FILES_OFF; + if (dcfg->upload_validates_files == NOT_SET) dcfg->upload_validates_files = 0; + + /* Misc */ + if (dcfg->data_dir == NOT_SET_P) dcfg->data_dir = NULL; + if (dcfg->webappid == NOT_SET_P) dcfg->webappid = "default"; +} + +/** + * TODO + */ +static const char *add_rule(cmd_parms *cmd, directory_config *dcfg, const char *p1, + const char *p2, const char *p3) +{ + char *my_error_msg = NULL; + msre_rule *rule = NULL; + extern msc_engine *modsecurity; + + /* Create a ruleset if one does not exist. */ + if ((dcfg->ruleset == NULL)||(dcfg->ruleset == NOT_SET_P)) { + dcfg->ruleset = msre_ruleset_create(modsecurity->msre, cmd->pool); + if (dcfg->ruleset == NULL) return FATAL_ERROR; + } + + /* Create the rule now. */ + rule = msre_rule_create(dcfg->ruleset, p1, p2, p3, &my_error_msg); + if (rule == NULL) { + return my_error_msg; + } + + /* Create default actionset if one does not already exist. */ + if (dcfg->tmp_default_actionset == NULL) { + dcfg->tmp_default_actionset = msre_actionset_create_default(modsecurity->msre); + if (dcfg->tmp_default_actionset == NULL) return FATAL_ERROR; + } + + /* Merge actions with the parent. */ + rule->actionset = msre_actionset_merge(modsecurity->msre, dcfg->tmp_default_actionset, + rule->actionset, 1); + + if (dcfg->tmp_chain_starter != NULL) { + /* This rule is part of a chain. */ + + /* Must NOT specify a disruptive action. */ + if (rule->actionset->intercept_action == NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: Disruptive actions can only " + "be specified by chain starter rules."); + } + + /* Must NOT specify a phase. */ + if (rule->actionset->phase == NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: Execution phases can only be " + "specified by chain starter rules."); + } + + /* Must NOT use metadata actions. */ + if ((rule->actionset->id != NOT_SET_P) + ||(rule->actionset->rev != NOT_SET_P) + ||(rule->actionset->msg != NOT_SET_P)) + { + return apr_psprintf(cmd->pool, "ModSecurity: Metadata actions (id, rev, msg) " + " can only be specified by chain starter rules."); + } + + /* Must NOT use skip. */ + if (rule->actionset->skip_count != NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: The skip action can only be used " + " by chain starter rules. "); + } + + rule->chain_starter = dcfg->tmp_chain_starter; + rule->actionset->phase = rule->chain_starter->actionset->phase; + } + + if (rule->actionset->is_chained != 1) { + /* If this rule is part of the chain but does + * not want more rules to follow in the chain + * then cut it (the chain). + */ + dcfg->tmp_chain_starter = NULL; + } else { + /* On the other hand, if this rule wants other + * rules to follow it, then start a new chain + * if there isn't one already. + */ + if (dcfg->tmp_chain_starter == NULL) { + dcfg->tmp_chain_starter = rule; + } + } + + /* Optimisation */ + if (strcasecmp(rule->op_name, "inspectFile") == 0) { + dcfg->upload_validates_files = 1; + } + + /* Add rule to the recipe. */ + if (msre_ruleset_rule_add(dcfg->ruleset, rule, rule->actionset->phase) < 0) { + return "Internal Error: Failed to add rule to the ruleset."; + } + + return NULL; +} + +/* -- Configuration directives -- */ + +static const char *cmd_action(cmd_parms *cmd, void *_dcfg, const char *p1) { + return add_rule(cmd, (directory_config *)_dcfg, "REQUEST_URI", "@unconditionalMatch", p1); +} + +static const char *cmd_argument_separator(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (strlen(p1) != 1) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid argument separator: %s", p1); + } + + dcfg->argument_separator = p1[0]; + + return NULL; +} + +static const char *cmd_audit_engine(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + if (strcasecmp(p1, "On") == 0) dcfg->auditlog_flag = AUDITLOG_ON; + else + if (strcasecmp(p1, "Off") == 0) dcfg->auditlog_flag = AUDITLOG_OFF; + else + if (strcasecmp(p1, "RelevantOnly") == 0) dcfg->auditlog_flag = AUDITLOG_RELEVANT; + else + return (const char *)apr_psprintf(cmd->pool, + "ModSecurity: Unrecognised parameter value for SecAuditEngine: %s", p1); + + return NULL; +} + +static const char *cmd_audit_log(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + dcfg->auditlog_name = (char *)p1; + + if (dcfg->auditlog_name[0] == '|') { + const char *pipe_name = ap_server_root_relative(cmd->pool, dcfg->auditlog_name + 1); + piped_log *pipe_log; + + pipe_log = ap_open_piped_log(cmd->pool, pipe_name); + if (pipe_log == NULL) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to open the audit log pipe: %s", + pipe_name); + } + dcfg->auditlog_fd = ap_piped_log_write_fd(pipe_log); + } + else { + const char *file_name = ap_server_root_relative(cmd->pool, dcfg->auditlog_name); + apr_status_t rc; + + rc = apr_file_open(&dcfg->auditlog_fd, file_name, + APR_WRITE | APR_APPEND | APR_CREATE | APR_BINARY, + CREATEMODE, cmd->pool); + + if (rc != APR_SUCCESS) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to open the audit log file: %s", + file_name); + } + } + + return NULL; +} + +static const char *cmd_audit_log_parts(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + if (is_valid_parts_specification((char *)p1) != 1) { + return apr_psprintf(cmd->pool, "Invalid parts specification for SecAuditLogParts: %s", p1); + } + + dcfg->auditlog_parts = (char *)p1; + return NULL; +} + +static const char *cmd_audit_log_relevant_status(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + dcfg->auditlog_relevant_regex = msc_pregcomp(cmd->pool, p1, PCRE_DOTALL, NULL, NULL); + if (dcfg->auditlog_relevant_regex == NULL) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid regular expression: %s", p1); + } + + return NULL; +} + +static const char *cmd_audit_log_type(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + if (strcasecmp(p1, "Serial") == 0) dcfg->auditlog_type = AUDITLOG_SERIAL; + else + if (strcasecmp(p1, "Concurrent") == 0) dcfg->auditlog_type = AUDITLOG_CONCURRENT; + else + return (const char *)apr_psprintf(cmd->pool, + "ModSecurity: Unrecognised parameter value for SecAuditLogType: %s", p1); + + return NULL; +} + +static const char *cmd_audit_log_storage_dir(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = _dcfg; + + dcfg->auditlog_storage_dir = ap_server_root_relative(cmd->pool, p1); + + return NULL; +} + +static const char *cmd_cookie_format(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (strcmp(p1, "0") == 0) dcfg->cookie_format = COOKIES_V0; + else + if (strcmp(p1, "1") == 0) dcfg->cookie_format = COOKIES_V1; + else { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid cookie format: %s", p1); + } + + return NULL; +} + +static const char *cmd_chroot_dir(cmd_parms *cmd, void *_dcfg, const char *p1) { + char cwd[1025] = ""; + + if (cmd->server->is_virtual) { + return "ModSecurity: SecChrootDir not allowed in VirtualHost"; + } + + chroot_dir = (char *)p1; + + if (getcwd(cwd, 1024) == NULL) { + return "ModSecurity: Failed to get the current working directory"; + } + + if (chdir(chroot_dir) < 0) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to chdir to %s, errno=%d (%s)", + chroot_dir, errno, strerror(errno)); + } + + if (chdir(cwd) < 0) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to chdir to %s, errno=%d (%s)", + cwd, errno, strerror(errno)); + } + + return NULL; +} + +static const char *cmd_data_dir(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (cmd->server->is_virtual) { + return "ModSecurity: SecDataDir not allowed in VirtualHost."; + } + + dcfg->data_dir = ap_server_root_relative(cmd->pool, p1); + + return NULL; +} + +static const char *cmd_debug_log(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + apr_status_t rc; + + dcfg->debuglog_name = ap_server_root_relative(cmd->pool, p1); + + rc = apr_file_open(&dcfg->debuglog_fd, dcfg->debuglog_name, + APR_WRITE | APR_APPEND | APR_CREATE | APR_BINARY, + CREATEMODE, cmd->pool); + + if (rc != APR_SUCCESS) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to open debug log file: %s", + dcfg->debuglog_name); + } + + return NULL; +} + +static const char *cmd_debug_log_level(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + dcfg->debuglog_level = atoi(p1); + if ((dcfg->debuglog_level >= 0)&&(dcfg->debuglog_level <= 9)) return NULL; + + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecDebugLogLevel: %s", p1); +} + +static const char *cmd_default_action(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + extern msc_engine *modsecurity; + char *my_error_msg = NULL; + + dcfg->tmp_default_actionset = msre_actionset_create(modsecurity->msre, p1, &my_error_msg); + if (dcfg->tmp_default_actionset == NULL) { + if (my_error_msg != NULL) return my_error_msg; + else return FATAL_ERROR; + } + + /* Must specify a disruptive action. */ + if (dcfg->tmp_default_actionset->intercept_action == NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: SecDefaultAction must specify a disruptive action."); + } + + /* Must specify a phase. */ + if (dcfg->tmp_default_actionset->phase == NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: SecDefaultAction must specify a phase."); + } + + /* Must not use metadata actions. */ + if ((dcfg->tmp_default_actionset->id != NOT_SET_P) + ||(dcfg->tmp_default_actionset->rev != NOT_SET_P) + ||(dcfg->tmp_default_actionset->msg != NOT_SET_P)) + { + return apr_psprintf(cmd->pool, "ModSecurity: SecDefaultAction must not " + "contain any metadata actions (id, rev, msg)."); + } + + /* Must not use chain. */ + if (dcfg->tmp_default_actionset->is_chained != NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: SecDefaultAction must not " + "contain a chain action."); + } + + /* Must not use skip. */ + if (dcfg->tmp_default_actionset->skip_count != NOT_SET) { + return apr_psprintf(cmd->pool, "ModSecurity: SecDefaultAction must not " + "contain a skip action."); + } + + return NULL; +} + +static const char *cmd_guardian_log(cmd_parms *cmd, void *_dcfg, const char *p1, const char *p2) { + extern char *guardianlog_name; + extern apr_file_t *guardianlog_fd; + extern char *guardianlog_condition; + + if (cmd->server->is_virtual) { + return "ModSecurity: SecGuardianLog not allowed in VirtualHost"; + } + + if (p2 != NULL) { + if (strncmp(p2, "env=", 4) != 0) { + return "ModSecurity: Error in condition clause"; + } + if ( (p2[4] == '\0') || ((p2[4] == '!')&&(p2[5] == '\0')) ) { + return "ModSecurity: Missing variable name"; + } + guardianlog_condition = apr_pstrdup(cmd->pool, p2 + 4); + } + + guardianlog_name = (char *)p1; + + if (guardianlog_name[0] == '|') { + const char *pipe_name = ap_server_root_relative(cmd->pool, guardianlog_name + 1); + piped_log *pipe_log; + + pipe_log = ap_open_piped_log(cmd->pool, pipe_name); + if (pipe_log == NULL) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to open the guardian log pipe: %s", + pipe_name); + } + guardianlog_fd = ap_piped_log_write_fd(pipe_log); + } + else { + const char *file_name = ap_server_root_relative(cmd->pool, guardianlog_name); + apr_status_t rc; + + rc = apr_file_open(&guardianlog_fd, file_name, + APR_WRITE | APR_APPEND | APR_CREATE | APR_BINARY, + CREATEMODE, cmd->pool); + + if (rc != APR_SUCCESS) { + return apr_psprintf(cmd->pool, "ModSecurity: Failed to open the guardian log file: %s", + file_name); + } + } + + return NULL; +} + +static const char *cmd_request_body_inmemory_limit(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + long int limit; + + if (dcfg == NULL) return NULL; + + limit = strtol(p1, NULL, 10); + if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecRequestBodyInMemoryLimit: %s", p1); + } + + dcfg->reqbody_inmemory_limit = limit; + + return NULL; +} + +static const char *cmd_request_body_limit(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + long int limit; + + if (dcfg == NULL) return NULL; + + limit = strtol(p1, NULL, 10); + if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecRequestBodyLimit: %s", p1); + } + + dcfg->reqbody_limit = limit; + + return NULL; +} + +static const char *cmd_request_body_access(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "on") == 0) dcfg->reqbody_access = 1; + else + if (strcasecmp(p1, "off") == 0) dcfg->reqbody_access = 0; + else + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecRequestBodyAccess: %s", p1); + + return NULL; +} + +static const char *cmd_response_body_access(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "on") == 0) dcfg->resbody_access = 1; + else + if (strcasecmp(p1, "off") == 0) dcfg->resbody_access = 0; + else + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecResponseBodyAccess: %s", p1); + + return NULL; +} + +static const char *cmd_response_body_limit(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + long int limit; + + limit = strtol(p1, NULL, 10); + if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecResponseBodyLimit: %s", p1); + } + + if (limit > RESPONSE_BODY_HARD_LIMIT) { + return apr_psprintf(cmd->pool, "ModSecurity: Response size limit can not exceed the hard limit: %li", RESPONSE_BODY_HARD_LIMIT); + } + + dcfg->of_limit = limit; + + return NULL; +} + +static const char *cmd_response_body_mime_type(cmd_parms *cmd, void *_dcfg, const char *_p1) { + directory_config *dcfg = (directory_config *)_dcfg; + char *p1 = apr_pstrdup(cmd->pool, _p1); + + // TODO check whether the parameter is a valid MIME type of "null" + + if ((dcfg->of_mime_types == NULL)||(dcfg->of_mime_types == NOT_SET_P)) { + dcfg->of_mime_types = apr_table_make(cmd->pool, 10); + } + + strtolower_inplace(p1); + apr_table_setn(dcfg->of_mime_types, p1, "1"); + + return NULL; +} + +static const char *cmd_response_body_mime_types_clear(cmd_parms *cmd, void *_dcfg) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + dcfg->of_mime_types_cleared = 1; + + if ((dcfg->of_mime_types != NULL)&&(dcfg->of_mime_types != NOT_SET_P)) { + apr_table_clear(dcfg->of_mime_types); + } + + return NULL; +} + +static const char *cmd_rule(cmd_parms *cmd, void *_dcfg, const char *p1, + const char *p2, const char *p3) +{ + return add_rule(cmd, (directory_config *)_dcfg, p1, p2, p3); +} + +static const char *cmd_rule_engine(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "on") == 0) dcfg->is_enabled = MODSEC_ENABLED; + else + if (strcasecmp(p1, "off") == 0) dcfg->is_enabled = MODSEC_DISABLED; + else + if (strcasecmp(p1, "detectiononly") == 0) dcfg->is_enabled = MODSEC_DETECTION_ONLY; + else + return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecRuleEngine: %s", p1); + + return NULL; +} + +/* +static const char *cmd_rule_import_by_id(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + rule_exception *re = apr_pcalloc(cmd->pool, sizeof(rule_exception)); + if (dcfg == NULL) return NULL; + + re->type = RULE_EXCEPTION_IMPORT_ID; + // TODO verify p1 + re->param = p1; + *(rule_exception **)apr_array_push(dcfg->rule_exceptions) = re; + + return NULL; +} + +static const char *cmd_rule_import_by_msg(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + rule_exception *re = apr_pcalloc(cmd->pool, sizeof(rule_exception)); + if (dcfg == NULL) return NULL; + + re->type = RULE_EXCEPTION_IMPORT_MSG; + // TODO verify p1 + re->param = p1; + *(rule_exception **)apr_array_push(dcfg->rule_exceptions) = re; + + return NULL; +} +*/ + +static const char *cmd_rule_inheritance(cmd_parms *cmd, void *_dcfg, int flag) { + directory_config *dcfg = (directory_config *)_dcfg; + if (dcfg == NULL) return NULL; + dcfg->rule_inheritance = flag; + return NULL; +} + +static const char *cmd_rule_remove_by_id(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + rule_exception *re = apr_pcalloc(cmd->pool, sizeof(rule_exception)); + if (dcfg == NULL) return NULL; + + re->type = RULE_EXCEPTION_REMOVE_ID; + re->param = p1; + *(rule_exception **)apr_array_push(dcfg->rule_exceptions) = re; + + /* Remove the corresponding rules from the context straight away. */ + msre_ruleset_rule_remove_with_exception(dcfg->ruleset, re); + + return NULL; +} + +static const char *cmd_rule_remove_by_msg(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + rule_exception *re = apr_pcalloc(cmd->pool, sizeof(rule_exception)); + if (dcfg == NULL) return NULL; + + re->type = RULE_EXCEPTION_REMOVE_MSG; + re->param = p1; + re->param_data = msc_pregcomp(cmd->pool, p1, 0, NULL, NULL); + if (re->param_data == NULL) { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid regular expression: %s", p1); + } + *(rule_exception **)apr_array_push(dcfg->rule_exceptions) = re; + + /* Remove the corresponding rules from the context straight away. */ + msre_ruleset_rule_remove_with_exception(dcfg->ruleset, re); + + #ifdef DEBUG_CONF + fprintf(stderr, "Added exception %x (%i %s) to dcfg %x.\n", re, re->type, re->param, dcfg); + #endif + + return NULL; +} + +static const char *cmd_server_signature(cmd_parms *cmd, void *_dcfg, const char *p1) { + if (cmd->server->is_virtual) { + return "ModSecurity: SecServerSignature not allowed in VirtualHost"; + } + new_server_signature = (char *)p1; + return NULL; +} + +static const char *cmd_tmp_dir(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "none") == 0) dcfg->tmp_dir = NULL; + else dcfg->tmp_dir = ap_server_root_relative(cmd->pool, p1); + + return NULL; +} + +static const char *cmd_upload_dir(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "none") == 0) dcfg->upload_dir = NULL; + else dcfg->upload_dir = ap_server_root_relative(cmd->pool, p1); + + return NULL; +} + +static const char *cmd_upload_keep_files(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + if (dcfg == NULL) return NULL; + + if (strcasecmp(p1, "on") == 0) { + dcfg->upload_keep_files = KEEP_FILES_ON; + } else + if (strcasecmp(p1, "off") == 0) { + dcfg->upload_keep_files = KEEP_FILES_OFF; + } else + if (strcasecmp(p1, "relevantonly") == 0) { + dcfg->upload_keep_files = KEEP_FILES_RELEVANT_ONLY; + } else { + return apr_psprintf(cmd->pool, "ModSecurity: Invalid setting for SecUploadKeepFiles: %s", + p1); + } + return NULL; +} + +static const char *cmd_web_app_id(cmd_parms *cmd, void *_dcfg, const char *p1) { + directory_config *dcfg = (directory_config *)_dcfg; + + // TODO enforce format (letters, digits, ., _, -) + dcfg->webappid = p1; + + return NULL; +} + + +/* -- Configuration directives definitions -- */ + +#define CMD_SCOPE_MAIN (RSRC_CONF) +#define CMD_SCOPE_ANY (RSRC_CONF | ACCESS_CONF) + +const command_rec module_directives[] = { + + AP_INIT_TAKE1 ( + "SecAction", + cmd_action, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecArgumentSeparator", + cmd_argument_separator, + NULL, + CMD_SCOPE_MAIN, + "character that will be used as separator when parsing application/x-www-form-urlencoded content." + ), + + AP_INIT_TAKE1 ( + "SecAuditEngine", + cmd_audit_engine, + NULL, + CMD_SCOPE_ANY, + "On, Off or RelevantOnly to determine the level of audit logging" + ), + + AP_INIT_TAKE1 ( + "SecAuditLog", + cmd_audit_log, + NULL, + CMD_SCOPE_ANY, + "The filename of the audit log file" + ), + + AP_INIT_TAKE1 ( + "SecAuditLogParts", + cmd_audit_log_parts, + NULL, + CMD_SCOPE_ANY, + "list of audit log parts that go into the log." + ), + + AP_INIT_TAKE1 ( + "SecAuditLogRelevantStatus", + cmd_audit_log_relevant_status, + NULL, + CMD_SCOPE_ANY, + "regular expression that will be used to determine if the response status is relevant for audit logging" + ), + + AP_INIT_TAKE1 ( + "SecAuditLogType", + cmd_audit_log_type, + NULL, + CMD_SCOPE_ANY, + "whether to use the old audit log format (Serial) or new (Concurrent)" + ), + + AP_INIT_TAKE1 ( + "SecAuditLogStorageDir", + cmd_audit_log_storage_dir, + NULL, + CMD_SCOPE_ANY, + "path to the audit log storage area; absolute, or relative to the root of the server" + ), + + AP_INIT_TAKE1 ( + "SecChrootDir", + cmd_chroot_dir, + NULL, + CMD_SCOPE_MAIN, + "Path of the directory to which server will be chrooted" + ), + + AP_INIT_TAKE1 ( + "SecCookieFormat", + cmd_cookie_format, + NULL, + CMD_SCOPE_ANY, + "version of the Cookie specification to use for parsing. Possible values are 0 and 1." + ), + + AP_INIT_TAKE1 ( + "SecDataDir", + cmd_data_dir, + NULL, + CMD_SCOPE_MAIN, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecDebugLog", + cmd_debug_log, + NULL, + CMD_SCOPE_ANY, + "path to the debug log file" + ), + + AP_INIT_TAKE1 ( + "SecDebugLogLevel", + cmd_debug_log_level, + NULL, + CMD_SCOPE_ANY, + "debug log level, which controls the verbosity of logging." + " Use values from 0 (no logging) to 9 (a *lot* of logging)." + ), + + AP_INIT_TAKE1 ( + "SecDefaultAction", + cmd_default_action, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE12 ( + "SecGuardianLog", + cmd_guardian_log, + NULL, + CMD_SCOPE_MAIN, + "The filename of the filter debugging log file" + ), + + AP_INIT_TAKE1 ( + "SecRequestBodyAccess", + cmd_request_body_access, + NULL, + CMD_SCOPE_ANY, + "On or Off" + ), + + AP_INIT_TAKE1 ( + "SecRequestBodyInMemoryLimit", + cmd_request_body_inmemory_limit, + NULL, + CMD_SCOPE_ANY, + "maximum request body size that will be placed in memory (except for POST urlencoded requests)." + ), + + AP_INIT_TAKE1 ( + "SecRequestBodyLimit", + cmd_request_body_limit, + NULL, + CMD_SCOPE_ANY, + "maximum request body size ModSecurity is allowed to access." + ), + + AP_INIT_TAKE1 ( + "SecResponseBodyAccess", + cmd_response_body_access, + NULL, + CMD_SCOPE_ANY, + "On or Off" + ), + + AP_INIT_TAKE1 ( + "SecResponseBodyLimit", + cmd_response_body_limit, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_ITERATE ( + "SecResponseBodyMimeType", + cmd_response_body_mime_type, + NULL, + CMD_SCOPE_ANY, + "adds given MIME types to the list of types that will be buffered on output" + ), + + AP_INIT_NO_ARGS ( + "SecResponseBodyMimeTypesClear", + cmd_response_body_mime_types_clear, + NULL, + CMD_SCOPE_ANY, + "clears the list of MIME types that will be buffered on output" + ), + + AP_INIT_TAKE23 ( + "SecRule", + cmd_rule, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecRuleEngine", + cmd_rule_engine, + NULL, + CMD_SCOPE_ANY, + "On or Off" + ), + + /* + AP_INIT_TAKE1 ( + "SecRuleImportById", + cmd_rule_import_by_id, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecRuleImportByMsg", + cmd_rule_import_by_msg, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + */ + + AP_INIT_FLAG ( + "SecRuleInheritance", + cmd_rule_inheritance, + NULL, + CMD_SCOPE_ANY, + "On or Off" + ), + + AP_INIT_ITERATE ( + "SecRuleRemoveById", + cmd_rule_remove_by_id, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_ITERATE ( + "SecRuleRemoveByMsg", + cmd_rule_remove_by_msg, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecServerSignature", + cmd_server_signature, + NULL, + CMD_SCOPE_MAIN, + "The new signature of the server" + ), + + AP_INIT_TAKE1 ( + "SecTmpDir", + cmd_tmp_dir, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecUploadDir", + cmd_upload_dir, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecUploadKeepFiles", + cmd_upload_keep_files, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + AP_INIT_TAKE1 ( + "SecWebAppId", + cmd_web_app_id, + NULL, + CMD_SCOPE_ANY, + "" // TODO + ), + + { NULL } +}; diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c new file mode 100644 index 00000000..6e2cee24 --- /dev/null +++ b/apache2/apache2_io.c @@ -0,0 +1,588 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: apache2_io.c,v 1.6 2007/01/23 16:08:15 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "modsecurity.h" +#include "apache2.h" + + +/* -- Input filter -- */ + +#if 0 +static void dummy_free_func(void *data) {} +#endif + +/** + * This request filter will forward the previously stored + * request body further down the chain (most likely to the + * processing module). + */ +apr_status_t input_filter(ap_filter_t *f, apr_bucket_brigade *bb_out, + ap_input_mode_t mode, apr_read_type_e block, apr_off_t nbytes) +{ + modsec_rec *msr = (modsec_rec *)f->ctx; + msc_data_chunk *chunk = NULL; + apr_bucket *bucket; + apr_status_t rc; + + if (msr == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server, + "ModSecurity: Internal error in input filter: msr is null."); + ap_remove_input_filter(f); + return APR_EGENERAL; + } + + if ((msr->if_status == IF_STATUS_COMPLETE)||(msr->if_status == IF_STATUS_NONE)) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Input forwarding already complete, skipping (f %x, r %x).", f, f->r); + } + ap_remove_input_filter(f); + return ap_get_brigade(f->next, bb_out, mode, block, nbytes); + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Forwarding input: mode=%i, block=%i, nbytes=%" APR_OFF_T_FMT + " (f %x, r %x).", mode, block, nbytes, f, f->r); + } + + if (msr->if_started_forwarding == 0) { + msr->if_started_forwarding = 1; + rc = modsecurity_request_body_retrieve_start(msr); + if (rc == -1) { + // TODO err + return APR_EGENERAL; + } + } + + rc = modsecurity_request_body_retrieve(msr, &chunk, (unsigned int)nbytes); + if (rc == -1) { + // TODO err + return APR_EGENERAL; + } + + if (chunk) { + /* Copy the data we received in the chunk */ + bucket = apr_bucket_heap_create(chunk->data, chunk->length, NULL, + f->r->connection->bucket_alloc); + + #if 0 + + It would seem that we cannot prevent other filters in the chain + from modifying data in-place. Hence we copy. + + if (chunk->is_permanent) { + /* Do not make a copy of the data we received in the chunk. */ + bucket = apr_bucket_heap_create(chunk->data, chunk->length, dummy_free_func, + f->r->connection->bucket_alloc); + } else { + /* Copy the data we received in the chunk. */ + bucket = apr_bucket_heap_create(chunk->data, chunk->length, NULL, + f->r->connection->bucket_alloc); + } + + #endif + + if (bucket == NULL) return APR_EGENERAL; + APR_BRIGADE_INSERT_TAIL(bb_out, bucket); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Forwarded %lu bytes.", chunk->length); + } + } + + if (rc == 0) { + modsecurity_request_body_retrieve_end(msr); + + bucket = apr_bucket_eos_create(f->r->connection->bucket_alloc); + if (bucket == NULL) return APR_EGENERAL; + APR_BRIGADE_INSERT_TAIL(bb_out, bucket); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Sent EOS."); + } + + /* We're done */ + msr->if_status = IF_STATUS_COMPLETE; + ap_remove_input_filter(f); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Input forwarding complete."); + } + } + + return APR_SUCCESS; +} + +/** + * Reads request body from a client. + */ +apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { + request_rec *r = msr->r; + unsigned int seen_eos; + apr_bucket_brigade *bb_in; + apr_bucket *bucket; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (msr->reqbody_should_exist != 1) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: This request does not have a body."); + } + return 0; + } + + if (msr->txcfg->reqbody_access != 1) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Request body access not enabled."); + } + return 0; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Reading request body."); + } + + if (modsecurity_request_body_start(msr) < 0) { + // TODO err + return -1; + } + + seen_eos = 0; + bb_in = apr_brigade_create(msr->mp, r->connection->bucket_alloc); + if (bb_in == NULL) return -1; + do { + apr_status_t rc; + + rc = ap_get_brigade(r->input_filters, bb_in, AP_MODE_READBYTES, APR_BLOCK_READ, HUGE_STRING_LEN); + if (rc != APR_SUCCESS) { + /* NOTE Apache returns -3 here when the request is too large + * and APR_EGENERAL when the client disconnects. + */ + switch(rc) { + case APR_TIMEUP : + return -4; + break; + case -3 : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: HTTP Error 413 - Request entity too large. (Most likely.)"); + rc = -3; + break; + case APR_EGENERAL : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: Client went away."); + rc = -2; + break; + default : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc)); + rc = -1; + break; + } + + if (*error_msg) msr_log(msr, 1, "%s", *error_msg); + + return rc; + } + + /* Loop through the buckets in the brigade in order + * to extract the size of the data available. + */ + for(bucket = APR_BRIGADE_FIRST(bb_in); + bucket != APR_BRIGADE_SENTINEL(bb_in); + bucket = APR_BUCKET_NEXT(bucket)) + { + const char *buf; + apr_size_t buflen; + + rc = apr_bucket_read(bucket, &buf, &buflen, APR_BLOCK_READ); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Input filter: Failed reading input / bucket (%i): %s", + rc, get_apr_error(msr->mp, rc)); + return -1; + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Input filter: Bucket type %s contains %i bytes.", + bucket->type->name, buflen); + } + + /* Check request body limit (should only trigger on chunked requests). */ + if (msr->reqbody_length + buflen > (apr_size_t)msr->txcfg->reqbody_limit) { + *error_msg = apr_psprintf(msr->mp, "Requests body is larger than the " + "configured limit (%lu).", msr->txcfg->reqbody_limit); + return -5; + } + + if (buflen != 0) { + if (modsecurity_request_body_store(msr, buf, buflen) < 0) { + // TODO err + return -1; + } + + msr->reqbody_length += buflen; + } + + if (APR_BUCKET_IS_EOS(bucket)) { + seen_eos = 1; + } + } + + apr_brigade_cleanup(bb_in); + } while(!seen_eos); + + modsecurity_request_body_end(msr); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Input filter: Completed receiving request body (length %lu).", + msr->reqbody_length); + } + + msr->if_status = IF_STATUS_WANTS_TO_RUN; + + return 1; +} + + +/* -- Output filter -- */ + +/** + * Sends a brigade with an error bucket down the filter chain. + */ +static apr_status_t send_error_bucket(ap_filter_t *f, int status) { + apr_bucket_brigade *brigade = NULL; + apr_bucket *bucket = NULL; + + brigade = apr_brigade_create(f->r->pool, f->r->connection->bucket_alloc); + if (brigade == NULL) return APR_EGENERAL; + bucket = ap_bucket_error_create(status, NULL, f->r->pool, f->r->connection->bucket_alloc); + if (bucket == NULL) return APR_EGENERAL; + APR_BRIGADE_INSERT_TAIL(brigade, bucket); + bucket = apr_bucket_eos_create(f->r->connection->bucket_alloc); + if (bucket == NULL) return APR_EGENERAL; + APR_BRIGADE_INSERT_TAIL(brigade, bucket); + + return ap_pass_brigade(f->next, brigade); +} + +/** + * Examines the configuration and the response MIME type + * in order to determine whether output buffering should + * run or not. + */ +static int output_filter_should_run(modsec_rec *msr, request_rec *r) { + char *content_type = NULL; + + /* Check configuration. */ + if (msr->txcfg->resbody_access != 1) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Response body buffering is not enabled."); + } + return 0; + } + + /* Check MIME type. */ + + if ((msr->txcfg->of_mime_types == NULL)||(msr->txcfg->of_mime_types == NOT_SET_P)) { + msr_log(msr, 1, "Output filter: MIME type structures are corrupted (internal error)."); + return -1; + } + + if (r->content_type != NULL) { + char *p = NULL; + + content_type = apr_pstrdup(msr->mp, r->content_type); + if (content_type == NULL) return -1; + + /* Hide the character encoding information + * if present. Sometimes the content type header + * looks like this "text/html; charset=xyz" ... + */ + p = strstr(content_type, ";"); + if (p != NULL) { + *p = '\0'; + } + + strtolower_inplace(content_type); + + if (strcmp(content_type, "text/html") == 0) { + /* Useful information to have should we later + * decide to do something with the HTML output. + */ + msr->resbody_contains_html = 1; + } + } else { + content_type = "null"; + } + + if (apr_table_get(msr->txcfg->of_mime_types, content_type) != NULL) return 1; + + return 0; +} + +/** + * Initialises the output filter. + */ +static apr_status_t output_filter_init(modsec_rec *msr, ap_filter_t *f, + apr_bucket_brigade *bb_in) +{ + request_rec *r = f->r; + const char *s_content_length = NULL; + apr_status_t rc; + + msr->of_brigade = apr_brigade_create(msr->mp, f->c->bucket_alloc); + if (msr->of_brigade == NULL) return -1; + msr->of_status = OF_STATUS_IN_PROGRESS; + + rc = output_filter_should_run(msr, r); + if (rc < 0) return -1; + if (rc == 0) return 0; + + /* Look up the Content-Length header to see if we know + * the amount of data coming our way. If we do and if + * it's too much we might want to stop processing right here. + */ + s_content_length = apr_table_get(r->headers_out, "Content-Length"); + if (s_content_length == NULL) { + /* Try this too, mod_cgi seems to put headers there */ + s_content_length = apr_table_get(r->err_headers_out, "Content-Length"); + } + + if (s_content_length != NULL) { + long int len; + + len = strtol(s_content_length, NULL, 10); + if ((len == LONG_MIN)||(len == LONG_MAX)||(len < 0)||(len >= 1073741824)) { + msr_log(msr, 1, "Output filter: Invalid Content-Length: %s", log_escape_nq(r->pool, (char *)s_content_length)); + return -1; + } + + if (len == 0) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Skipping response since Content-Length is zero."); + } + return 0; + } + + if (len > msr->txcfg->of_limit) { + msr_log(msr, 1, "Output filter: Content-Length (%s) over the limit (%lu).", log_escape_nq(r->pool, (char *)s_content_length), msr->txcfg->of_limit); + return -2; + } + } + + return 1; +} + +/** + * Output filter. + */ +apr_status_t output_filter(ap_filter_t *f, apr_bucket_brigade *bb_in) { + request_rec *r = f->r; + modsec_rec *msr = (modsec_rec *)f->ctx; + apr_bucket *bucket; + apr_status_t rc; + + if (msr == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, f->r->server, + "ModSecurity: Internal Error: msr is null in output filter."); + ap_remove_output_filter(f); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Receiving output (f %x, r %x).", f, f->r); + } + + /* Initialise on first invocation */ + if (msr->of_status == OF_STATUS_NOT_STARTED) { + /* Update our context from the request structure. */ + msr->r = r; + msr->response_status = r->status; + msr->status_line = ((r->status_line != NULL) + ? r->status_line : ap_get_status_line(r->status)); + msr->response_protocol = get_response_protocol(r); + msr->response_headers = apr_table_overlay(msr->mp, r->err_headers_out, r->headers_out); + + /* Process phase RESPONSE_HEADERS */ + rc = modsecurity_process_phase(msr, PHASE_RESPONSE_HEADERS); + if (rc < 0) { /* error */ + ap_remove_output_filter(f); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + if (rc > 0) { /* transaction needs to be interrupted */ + int status = perform_interception(msr); + if (status != DECLINED) { /* DECLINED means we allow-ed the request. */ + ap_remove_output_filter(f); + return send_error_bucket(f, status); + } + } + + /* Decide whether to observe the response body. */ + rc = output_filter_init(msr, f, bb_in); + switch(rc) { + case -2 : /* response too large */ + case -1 : /* error */ + /* there's something wrong with this response */ + ap_remove_output_filter(f); + msr->of_status = OF_STATUS_COMPLETE; + msr->resbody_status = RESBODY_STATUS_ERROR; + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + case 0 : + /* We do not want to observe this response body + * but we need to remain attached to observe + * when it is completed so that we can run + * the RESPONSE_BODY phase. + */ + msr->of_skipping = 1; + msr->resbody_status = RESBODY_STATUS_NOT_READ; + break; + default : + /* Continue (observe the response body). */ + break; + } + } else + if (msr->of_status == OF_STATUS_COMPLETE) { + msr_log(msr, 1, "Output filter: Internal error: output filtering complete yet filter was invoked."); + ap_remove_output_filter(f); + return APR_EGENERAL; + } + + /* Loop through the buckets in the brigade in order + * to extract the size of the data available. + */ + for(bucket = APR_BRIGADE_FIRST(bb_in); + bucket != APR_BRIGADE_SENTINEL(bb_in); + bucket = APR_BUCKET_NEXT(bucket)) { + const char *buf; + apr_size_t buflen; + + if (msr->of_skipping == 0) { + rc = apr_bucket_read(bucket, &buf, &buflen, APR_BLOCK_READ); + if (rc != APR_SUCCESS) { + msr->of_status = OF_STATUS_COMPLETE; + msr->resbody_status = RESBODY_STATUS_ERROR; + msr_log(msr, 1, "Output filter: Failed to read bucket (rc %i): %s", + rc, get_apr_error(r->pool, rc)); + ap_remove_output_filter(f); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Output filter: Bucket type %s contains %i bytes.", + bucket->type->name, buflen); + } + + if (msr->resbody_length > (apr_size_t)msr->txcfg->of_limit) { + msr_log(msr, 1, "Output filter: Response body too large (over limit of %lu, total length not known).", + msr->txcfg->of_limit); + msr->of_status = OF_STATUS_COMPLETE; + msr->resbody_status = RESBODY_STATUS_PARTIAL; + ap_remove_output_filter(f); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + msr->resbody_length += buflen; + } + + if (APR_BUCKET_IS_EOS(bucket)) { + msr->of_done_reading = 1; + } + } + + /* Add buckets in this brigade to the brigade + * we have in the context, but only if we actually + * want to keep the response body. + */ + if (msr->of_skipping == 0) { + ap_save_brigade(f, &msr->of_brigade, &bb_in, msr->mp); + + if (msr->of_done_reading == 0) { + /* We are done for now. We will be called again with more data. */ + return APR_SUCCESS; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Completed receiving response (length %lu).", + msr->resbody_length); + } + } else { + if (msr->of_done_reading == 0) { + return ap_pass_brigade(f->next, bb_in); + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Completed receiving response."); + } + } + + /* We're not coming back here. */ + msr->of_status = OF_STATUS_COMPLETE; + ap_remove_output_filter(f); + + if (msr->of_skipping == 0) { + /* We've done with reading, it's time to inspect the data. */ + msr->resbody_status = RESBODY_STATUS_READ_BRIGADE; + + if (msr->resbody_length + 1 <= 0) { + msr_log(msr, 1, "Output filter: Invalid response length: %lu", msr->resbody_length); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + msr->resbody_data = apr_palloc(msr->mp, msr->resbody_length + 1); + if (msr->resbody_data == NULL) { + msr_log(msr, 1, "Output filter: Response body data memory allocation failed. Asked for: %li", + msr->resbody_length + 1); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + + // TODO Why does the function below take pointer to length? Will it modify it? + + rc = apr_brigade_flatten(msr->of_brigade, msr->resbody_data, &msr->resbody_length); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Output filter: Failed to flatten brigade (%i): %s", rc, + get_apr_error(r->pool, rc)); + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + msr->resbody_data[msr->resbody_length] = '\0'; + msr->resbody_status = RESBODY_STATUS_READ; + } + + /* Process phase RESPONSE_BODY */ + rc = modsecurity_process_phase(msr, PHASE_RESPONSE_BODY); + if (rc < 0) { + return send_error_bucket(f, HTTP_INTERNAL_SERVER_ERROR); + } + if (rc > 0) { + int status = perform_interception(msr); + if (status != DECLINED) { /* DECLINED means we allow-ed the request. */ + return send_error_bucket(f, status); + } + } + + if (msr->of_skipping == 0) { + record_time_checkpoint(msr, 3); + + rc = ap_pass_brigade(f->next, msr->of_brigade); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Output filter: Error while forwarding response data (%i): %s", + rc, get_apr_error(msr->mp, rc)); + return rc; + } + } + + /* Another job well done! */ + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Output filter: Output forwarding complete."); + } + + if (msr->of_skipping == 0) { + return APR_SUCCESS; + } else { + return ap_pass_brigade(f->next, bb_in); + } +} diff --git a/apache2/apache2_util.c b/apache2/apache2_util.c new file mode 100644 index 00000000..817ddc03 --- /dev/null +++ b/apache2/apache2_util.c @@ -0,0 +1,322 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: apache2_util.c,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "modsecurity.h" +#include "apache2.h" +#include "http_core.h" +#include "util_script.h" + +/** + * Execute system command. First line of the output will be returned in + * the "output" parameter. + */ +int apache2_exec(modsec_rec *msr, const char *command, const char **argv, char **output) { + apr_procattr_t *procattr = NULL; + apr_proc_t *procnew = NULL; + apr_status_t rc = APR_SUCCESS; + const char *const *env = NULL; + apr_file_t *script_out = NULL; + request_rec *r = msr->r; + + if (argv == NULL) { + argv = apr_pcalloc(r->pool, 3 * sizeof(char *)); + argv[0] = command; + argv[1] = NULL; + } + + ap_add_cgi_vars(r); + ap_add_common_vars(r); + + /* PHP hack, getting around its silly security checks. */ + apr_table_add(r->subprocess_env, "PATH_TRANSLATED", command); + apr_table_add(r->subprocess_env, "REDIRECT_STATUS", "302"); + + env = (const char * const *)ap_create_environment(r->pool, r->subprocess_env); + if (env == NULL) { + msr_log(msr, 1, "Exec: Unable to create environment."); + return -1; + } + + procnew = apr_pcalloc(r->pool, sizeof(*procnew)); + if (procnew == NULL) { + msr_log(msr, 1, "Exec: Unable to allocate %i bytes.", sizeof(*procnew)); + return -1; + } + + apr_procattr_create(&procattr, r->pool); + if (procattr == NULL) { + msr_log(msr, 1, "Exec: Unable to create procattr."); + return -1; + } + + apr_procattr_io_set(procattr, APR_NO_PIPE, APR_FULL_BLOCK, APR_NO_PIPE); + + rc = apr_proc_create(procnew, command, argv, env, procattr, r->pool); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Exec: Execution failed: %s (%s)", log_escape_nq(r->pool, command), + get_apr_error(r->pool, rc)); + return -1; + } + + apr_pool_note_subprocess(r->pool, procnew, APR_KILL_AFTER_TIMEOUT); + + script_out = procnew->out; + if (!script_out) { + msr_log(msr, 1, "Exec: Failed to get script output pipe."); + return -1; + } + + apr_file_pipe_timeout_set(script_out, r->server->timeout); + + /* Now read from the pipe. */ + { + char buf[260] = ""; + char *p = buf; + apr_size_t nbytes = 255; + apr_status_t rc2; + + rc2 = apr_file_read(script_out, buf, &nbytes); + if (rc2 == APR_SUCCESS) { + buf[nbytes] = 0; + + /* if there is more than one line ignore them */ + while(*p != 0) { + if (*p == 0x0a) *p = 0; + p++; + } + + msr_log(msr, 4, "Exec: First line from script output: \"%s\"", + log_escape(r->pool, buf)); + + if (output != NULL) *output = apr_pstrdup(r->pool, buf); + + /* Soak up the remaining data. */ + nbytes = 255; + while(apr_file_read(script_out, buf, &nbytes) == APR_SUCCESS) nbytes = 255; + } else { + msr_log(msr, 1, "Exec: Execution failed: %s (%s)", log_escape_nq(r->pool, command), + get_apr_error(r->pool, rc2)); + return -1; + } + } + + apr_proc_wait(procnew, NULL, NULL, APR_WAIT); + + return 1; +} + +/** + * Record the current time and store for later. + */ +void record_time_checkpoint(modsec_rec *msr, int checkpoint_no) { + char note[100], note_name[100]; + apr_time_t now; + + now = apr_time_now(); + switch(checkpoint_no) { + case 1 : + msr->time_checkpoint_1 = now; + break; + case 2 : + msr->time_checkpoint_2 = now; + break; + case 3 : + msr->time_checkpoint_3 = now; + break; + default : + msr_log(msr, 1, "Internal Error: Unknown checkpoint: %i", checkpoint_no); + return; + break; + } + + /* Apache-specific stuff. */ + apr_snprintf(note, 99, "%" APR_TIME_T_FMT, (now - msr->request_time)); + apr_snprintf(note_name, 99, "mod_security-time%i", checkpoint_no); + apr_table_set(msr->r->notes, note_name, note); + + msr_log(msr, 4, "Time #%i: %s", checkpoint_no, note); +} + +/** + * Returns a new string that contains the error + * message for the given return code. + */ +char *get_apr_error(apr_pool_t *p, apr_status_t rc) { + char *text = apr_pcalloc(p, 201); + if (text == NULL) return NULL; + apr_strerror(rc, text, 200); + return text; +} + +/** + * Retrieve named environment variable. + */ +char *get_env_var(request_rec *r, char *name) { + char *result = (char *)apr_table_get(r->notes, name); + + if (result == NULL) { + result = (char *)apr_table_get(r->subprocess_env, name); + } + + if (result == NULL) { + result = getenv(name); + } + + return result; +} + +/** + * Internal log helper function. Use msr_log instead. + */ +void internal_log(request_rec *r, directory_config *dcfg, modsec_rec *msr, + int level, const char *text, va_list ap) +{ + apr_size_t nbytes, nbytes_written; + apr_file_t *debuglog_fd = NULL; + int filter_debug_level = 0; + char str1[1024] = ""; + char str2[1256] = ""; + + /* Find the logging FD and look up the logging level in the configuration. */ + if (dcfg != NULL) { + if ((dcfg->debuglog_fd != NULL)&&(dcfg->debuglog_fd != NOT_SET_P)) { + debuglog_fd = dcfg->debuglog_fd; + } + if (dcfg->debuglog_level != NOT_SET) filter_debug_level = dcfg->debuglog_level; + } + + /* Return immediately if we don't have where to write + * or if the log level of the message is higher than + * wanted in the log. + */ + if ((level > 3)&&( (debuglog_fd == NULL) || (level > filter_debug_level) )) return; + + /* Construct the message. */ + apr_vsnprintf(str1, sizeof(str1), text, ap); + apr_snprintf(str2, sizeof(str2), "[%s] [%s/sid#%lx][rid#%lx][%s][%i] %s\n", + current_logtime(msr->mp), ap_get_server_name(r), (unsigned long)(r->server), + (unsigned long)r, ((r->uri == NULL) ? "" : log_escape_nq(msr->mp, r->uri)), + level, str1); + + /* Write to the debug log. */ + if ((debuglog_fd != NULL)&&(level <= filter_debug_level)) { + nbytes = strlen(str2); + apr_file_write_full(debuglog_fd, str2, nbytes, &nbytes_written); + } + + /* Send message levels 1-3 to the Apache error log too. */ + if (level <= 3) { + char *unique_id = (char *)get_env_var(r, "UNIQUE_ID"); + char *hostname = (char *)r->hostname; + + if (unique_id != NULL) { + unique_id = apr_psprintf(msr->mp, " [unique_id \"%s\"]", + log_escape(msr->mp, unique_id)); + } + else unique_id = ""; + + if (hostname != NULL) { + hostname = apr_psprintf(msr->mp, " [hostname \"%s\"]", + log_escape(msr->mp, hostname)); + } + else hostname = ""; + + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, r->server, + "[client %s] ModSecurity: %s%s [uri \"%s\"]%s", r->connection->remote_ip, str1, + hostname, log_escape(msr->mp, r->unparsed_uri), unique_id); + + /* Add this message to the list. */ + if (msr != NULL) { + *(const char **)apr_array_push(msr->alerts) = apr_pstrdup(msr->mp, str1); + } + } + + return; +} + +/** + * Logs one message at the given level to the debug log (and to the + * Apache error log if the message is important enough. + */ +void msr_log(modsec_rec *msr, int level, const char *text, ...) { + va_list ap; + + va_start(ap, text); + internal_log(msr->r, msr->txcfg, msr, level, text, ap); + va_end(ap); +} + +/** + * Converts an Apache error log message into one line of text. + */ +char *format_error_log_message(apr_pool_t *mp, error_message *em) { + char *s_file = "", *s_line = "", *s_level = ""; + char *s_status = "", *s_message = ""; + char *msg = NULL; + + if (em == NULL) return NULL; + + if (em->file != NULL) { + s_file = apr_psprintf(mp, "[file \"%s\"] ", + log_escape(mp, (char *)em->file)); + if (s_file == NULL) return NULL; + } + + if (em->line > 0) { + s_line = apr_psprintf(mp, "[line %i] ", em->line); + if (s_line == NULL) return NULL; + } + + s_level = apr_psprintf(mp, "[level %i] ", em->level); + if (s_level == NULL) return NULL; + + if (em->status != 0) { + s_status = apr_psprintf(mp, "[status %i] ", em->status); + if (s_status == NULL) return NULL; + } + + if (em->message != NULL) { + s_message = log_escape_nq(mp, em->message); + if (s_message == NULL) return NULL; + } + + msg = apr_psprintf(mp, "%s%s%s%s%s", s_file, s_line, s_level, s_status, s_message); + if (msg == NULL) return NULL; + + return msg; +} + +/** + * Determines the reponse protocol Apache will use (or has used) + * to respond to the given request. + */ +const char *get_response_protocol(request_rec *r) { + int proto_num = r->proto_num; + + if (r->assbackwards) { + return NULL; + } + + if (proto_num > HTTP_VERSION(1,0) + && apr_table_get(r->subprocess_env, "downgrade-1.0")) + { + proto_num = HTTP_VERSION(1,0); + } + + if (proto_num == HTTP_VERSION(1,0) + && apr_table_get(r->subprocess_env, "force-response-1.0")) + { + return "HTTP/1.0"; + } + + return AP_SERVER_PROTOCOL; +} diff --git a/apache2/api/README b/apache2/api/README new file mode 100644 index 00000000..e5ca7006 --- /dev/null +++ b/apache2/api/README @@ -0,0 +1,26 @@ + +This directory contains two examples how you can extend +ModSecurity without having to touch it directly, simply +by creating custom Apache modules. + +1) + +Module mod_tfn_reverse.c creates a custom transformation +function "reverse" that reverses the content it receives +on input. + +To compile simply do: + + apxs -cia mod_tfn_reverse.c + +2) + +Module mod_op_strstr.c creates a custom operator "strstr" +that implements fast matching using the Boyer-Moore-Horspool +algorithm. + +Compiling this module is more involved because it requires +access to ModSecurity structures. For example: + + apxs -I -I/usr/include/libxml2 -cia mod_op_strstr.c + diff --git a/apache2/api/mod_op_strstr.c b/apache2/api/mod_op_strstr.c new file mode 100644 index 00000000..df9a6966 --- /dev/null +++ b/apache2/api/mod_op_strstr.c @@ -0,0 +1,176 @@ + +#include "httpd.h" +#include "http_core.h" +#include "http_config.h" +#include "http_log.h" +#include "http_protocol.h" +#include "ap_config.h" +#include "apr_optional.h" + +#include "modsecurity.h" + +#define ALPHABET_SIZE 256 +#define MAX_PATTERN_SIZE 64 + +APR_DECLARE_OPTIONAL_FN(void, modsec_register_operator, + (const char *name, void *fn_init, void *fn_exec)); + +static void initBoyerMooreHorspool(const char *pattern, int patlength, + int *bm_badcharacter_array); + +static int BoyerMooreHorspool(const char *pattern, int patlength, + const char *text, int textlen, int *bm_badcharacter_array); + +/** + * Operator parameter initialisation entry point. + */ +static int op_strstr_init(msre_rule *rule, char **error_msg) { + /* Operator initialisation function will be called once per + * statement where operator is used. It is meant to be used + * to check the parameters to see whether they are present + * and if they are in the correct format. + */ + + /* In this example we just look for a simple non-empty parameter. */ + if ((rule->op_param == NULL)||(strlen(rule->op_param) == 0)) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Missing parameter for operator 'strstr'."); + return 0; /* ERROR */ + } + + /* If you need to transform the data in the parameter into something + * else you should do that here. Simply create a new structure to hold + * the transformed data and place the pointer to it into rule->op_param_data. + * You will have access to this pointer later on. + */ + rule->op_param_data = apr_pcalloc(rule->ruleset->mp, ALPHABET_SIZE * sizeof(int)); + initBoyerMooreHorspool(rule->op_param, strlen(rule->op_param), (int *)rule->op_param_data); + + /* OK */ + return 1; +} + +/** + * Operator execution entry point. + */ +static int op_strstr_exec(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) { + char *valuecopy = NULL; + + /* Here we need to inspect the contents of the supplied variable. */ + + /* In a general case it is possible for the value + * to be NULL. What you need to do in this case + * depends on your operator. In this example we return + * a "no match" response. + */ + if (var->value == NULL) return 0; /* No match. */ + + /* Another thing to note is that variables are not C strings, + * meaning the NULL byte is not used to determine the end + * of the string. Variable length var->value_len should be + * used for this purpose. + */ + + if (BoyerMooreHorspool(rule->op_param, strlen(rule->op_param), + var->value, var->value_len, (int *)rule->op_param_data) >= 0) + { + return 1; /* Match. */ + } + + return 0; /* No match. */ +} + +static int hook_pre_config(apr_pool_t *mp, apr_pool_t *mp_log, apr_pool_t *mp_temp) { + void (*fn)(const char *name, void *fn_init, void *fn_exec); + + /* Look for the registration function + * exported by ModSecurity. + */ + fn = APR_RETRIEVE_OPTIONAL_FN(modsec_register_operator); + if (fn) { + /* Use it to register our new + * transformation function under the + * name "reverse". + */ + fn("strstr", (void *)op_strstr_init, (void *)op_strstr_exec); + } + + return OK; +} + +static void register_hooks(apr_pool_t *p) { + ap_hook_pre_config(hook_pre_config, NULL, NULL, APR_HOOK_LAST); +} + +/* Dispatch list for API hooks */ +module AP_MODULE_DECLARE_DATA op_strstr_module = { + STANDARD20_MODULE_STUFF, + NULL, /* create per-dir config structures */ + NULL, /* merge per-dir config structures */ + NULL, /* create per-server config structures */ + NULL, /* merge per-server config structures */ + NULL, /* table of config file commands */ + register_hooks /* register hooks */ +}; + +/* + +This example uses an implementation Boyer-Moore-Horspool +matching algorithm as implemented in Streamline (http://ffpf.sourceforge.net). + +Copyright (c) 2004-2006, Vrije Universiteit Amsterdam +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +Neither the name of the Vrije Universiteit nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +*/ + +static void precompute_badcharacter(const char *pattern, int patlength, + int bm_badcharacter_array[]) +{ + int i; + + for (i = 0; i < ALPHABET_SIZE; ++i) { + bm_badcharacter_array[i] = patlength; + } + + for (i = 0; i < patlength - 1; ++i){ + bm_badcharacter_array[(uint8_t)pattern[i]] = patlength - i - 1; + } +} + +static void initBoyerMooreHorspool(const char *pattern, int patlength, + int *bm_badcharacter_array) +{ + precompute_badcharacter(pattern, + (patlength < MAX_PATTERN_SIZE ? patlength : MAX_PATTERN_SIZE), bm_badcharacter_array); +} + +static int BoyerMooreHorspool(const char *pattern, int patlength, + const char *text, int textlen, int *bm_badcharacter_array) +{ + int j; + char c; + + j = 0; + while (j <= textlen - patlength) { + c = text[j + patlength - 1]; + if (pattern[patlength - 1] == c && memcmp(pattern, text + j, patlength - 1) == 0) { + return j; + } + j += bm_badcharacter_array[(uint8_t)c]; + } + + return -1; +} diff --git a/apache2/api/mod_tfn_reverse.c b/apache2/api/mod_tfn_reverse.c new file mode 100644 index 00000000..6ba94d5e --- /dev/null +++ b/apache2/api/mod_tfn_reverse.c @@ -0,0 +1,86 @@ + +#include "httpd.h" +#include "http_core.h" +#include "http_config.h" +#include "http_log.h" +#include "http_protocol.h" +#include "ap_config.h" +#include "apr_optional.h" + +APR_DECLARE_OPTIONAL_FN(void, modsec_register_tfn, (const char *name, void *fn)); + +/** + * This function will be invoked by + * ModSecurity to transform input. + */ +static int reverse(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + /* Transformation functions can choose to do their + * thing in-place, overwriting the existing content. This + * is normally possible only if the transformed content + * is of equal length or shorter. + * + * If you need to expand the content use the temporary + * memory pool mptmp to allocate the space. + */ + + /* Reverse the string in place, but only if it's long enough. */ + if (input_len > 1) { + long int i = 0; + long int j = input_len - 1; + while(i < j) { + char c = input[i]; + input[i] = input[j]; + input[j] = c; + i++; + j--; + } + } + + /* Tell ModSecurity about the content + * we have generated. In this case we + * merely point back to the input buffer. + */ + *rval = (char *)input; + *rval_len = input_len; + + /* Must return 1 if the content was + * changed, or 0 otherwise. + */ + return 1; +} + +static int hook_pre_config(apr_pool_t *mp, apr_pool_t *mp_log, apr_pool_t *mp_temp) { + void (*fn)(const char *name, void *fn); + + /* Look for the registration function + * exported by ModSecurity. + */ + fn = APR_RETRIEVE_OPTIONAL_FN(modsec_register_tfn); + if (fn) { + /* Use it to register our new + * transformation function under the + * name "reverse". + */ + fn("reverse", (void *)reverse); + } + + return OK; +} + +static void register_hooks(apr_pool_t *p) { + ap_hook_pre_config(hook_pre_config, NULL, NULL, APR_HOOK_LAST); +} + +/* Dispatch list for API hooks */ +module AP_MODULE_DECLARE_DATA tfn_reverse_module = { + STANDARD20_MODULE_STUFF, + NULL, /* create per-dir config structures */ + NULL, /* merge per-dir config structures */ + NULL, /* create per-server config structures */ + NULL, /* merge per-server config structures */ + NULL, /* table of config file commands */ + register_hooks /* register hooks */ +}; + diff --git a/apache2/mod_security2.c b/apache2/mod_security2.c new file mode 100644 index 00000000..0c7bcaa7 --- /dev/null +++ b/apache2/mod_security2.c @@ -0,0 +1,991 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: mod_security2.c,v 1.11 2006/12/15 15:06:04 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "http_core.h" +#include "http_request.h" + +#include "modsecurity.h" +#include "apache2.h" +#include "msc_logging.h" +#include "msc_util.h" + + +/* ModSecurity structure */ + +msc_engine DSOLOCAL *modsecurity = NULL; + + +/* Global module variables; these are used for the Apache-specific functionality */ + +char DSOLOCAL *chroot_dir = NULL; + +unsigned int DSOLOCAL chroot_completed = 0; + +char DSOLOCAL *new_server_signature = NULL; + +char DSOLOCAL *real_server_signature = NULL; + +char DSOLOCAL *guardianlog_name = NULL; + +apr_file_t DSOLOCAL *guardianlog_fd = NULL; + +char DSOLOCAL *guardianlog_condition = NULL; + + +/* -- Miscellaneous functions -- */ + +/** + * Intercepts transaction, using the method specified + * in the structure itself. MUST return an HTTP status code, + * which will be used to terminate the transaction. + */ +int perform_interception(modsec_rec *msr) { + msre_actionset *actionset = NULL; + const char *message = NULL; + const char *phase_text = ""; + int status = DECLINED; + int log_level = 1; + + /* Sanity checks first. */ + + if (msr->was_intercepted == 0) { + msr_log(msr, 1, "Internal Error: Asked to intercept request but was_intercepted is zero"); + return DECLINED; + } + + if (msr->phase > 4) { + msr_log(msr, 1, "Internal Error: Asked to intercept request in phase %i.", msr->phase); + return DECLINED; + } + + /* OK, we're good to go. */ + + actionset = msr->intercept_actionset; + phase_text = apr_psprintf(msr->mp, " (phase %i)", msr->phase); + + /* By default we log at level 1 but we switch to 4 + * if a nolog action was used to hide the message. + */ + log_level = (actionset->log != 1) ? 4 : 1; + + /* Pause the request first (if configured to do so). */ + if (actionset->intercept_pause) { + msr_log(msr, (log_level > 3 ? log_level : log_level + 1), "Pausing transaction for " + "%i msec.", actionset->intercept_pause); + /* apr_sleep accepts microseconds */ + apr_sleep((apr_interval_time_t)(actionset->intercept_pause * 1000)); + } + + /* Determine how to respond and prepare the log message. */ + switch(actionset->intercept_action) { + case ACTION_DENY : + if (actionset->intercept_status != 0) { + status = actionset->intercept_status; + message = apr_psprintf(msr->mp, "Access denied with code %i%s.", status, + phase_text); + } else { + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Internal Error: Invalid status code requested %i).", phase_text, + actionset->intercept_status); + } + break; + + case ACTION_PROXY : + if (msr->phase < 3) { + if (ap_find_linked_module("mod_proxy.c") == NULL) { + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Configuration Error: Proxy action to %s requested but mod_proxy not found).", + phase_text, log_escape_nq(msr->mp, actionset->intercept_uri)); + } else { + msr->r->filename = apr_psprintf(msr->mp, "proxy:%s", actionset->intercept_uri); + msr->r->proxyreq = PROXYREQ_REVERSE; + msr->r->handler = "proxy-server"; + status = OK; + message = apr_psprintf(msr->mp, "Access denied using proxy to %s%s.", + phase_text, log_escape_nq(msr->mp, actionset->intercept_uri)); + } + } else { + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Configuration Error: Proxy action requested but it does not work in output phases).", + phase_text); + } + break; + + case ACTION_DROP : + /* ENH This does not seem to work on Windows. Is there a + * better way to drop a connection anyway? + */ + #ifndef WIN32 + { + extern module core_module; + apr_socket_t *csd = ap_get_module_config(msr->r->connection->conn_config, + &core_module); + + if (csd) { + if (apr_socket_close(csd) == APR_SUCCESS) { + status = HTTP_FORBIDDEN; + message = apr_psprintf(msr->mp, "Access denied with connection close%s.", + phase_text); + } else { + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Error: Connection drop requested but failed to close the " + " socket).", phase_text); + } + } else { + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Error: Connection drop requested but socket not found.", + phase_text); + } + } + #else + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Error: Connection drop not implemented on this platform).", + phase_text); + #endif + break; + + case ACTION_REDIRECT : + apr_table_setn(msr->r->headers_out, "Location", actionset->intercept_uri); + if ((actionset->intercept_status == 301)||(actionset->intercept_status == 302) + ||(actionset->intercept_status == 303)||(actionset->intercept_status == 307)) + { + status = actionset->intercept_status; + } else { + status = HTTP_MOVED_TEMPORARILY; + } + message = apr_psprintf(msr->mp, "Access denied with redirection to %s using " + "status %i%s.", log_escape_nq(msr->mp, actionset->intercept_uri), status, + phase_text); + break; + + case ACTION_ALLOW : + status = DECLINED; + message = apr_psprintf(msr->mp, "Access allowed%s.", phase_text); + break; + + default : + log_level = 1; + status = HTTP_INTERNAL_SERVER_ERROR; + message = apr_psprintf(msr->mp, "Access denied with code 500%s " + "(Internal Error: invalid interception action %i).", + phase_text, actionset->intercept_action); + break; + } + + /* Log the message now. */ + msc_alert(msr, log_level, actionset, message, msr->intercept_message); + + return status; +} + +/** + * Retrieves a previously stored transaction context by + * looking at the main request, and the previous requests. + */ +static modsec_rec *retrieve_tx_context(const request_rec *r) { + modsec_rec *msr = NULL; + request_rec *rx = NULL; + + /* Look in the current request first. */ + msr = (modsec_rec *)apr_table_get(r->notes, NOTE_MSR); + if (msr != NULL) { + return msr; + } + + /* If this is a subrequest then look in the main request. */ + if (r->main != NULL) { + msr = (modsec_rec *)apr_table_get(r->main->notes, NOTE_MSR); + if (msr != NULL) { + return msr; + } + } + + /* If the request was redirected then look in the previous requests. */ + rx = r->prev; + while(rx != NULL) { + msr = (modsec_rec *)apr_table_get(rx->notes, NOTE_MSR); + if (msr != NULL) { + return msr; + } + rx = rx->prev; + } + + return NULL; +} + +/** + * Stores transaction context where it can be found in subsequent + * phases, redirections, or subrequests. + */ +static void store_tx_context(modsec_rec *msr, request_rec *r) { + apr_table_setn(r->notes, NOTE_MSR, (void *)msr); +} + +/** + * Creates a new transaction context. + */ +static modsec_rec *create_tx_context(request_rec *r) { + apr_allocator_t *allocator = NULL; + modsec_rec *msr = NULL; + + msr = (modsec_rec *)apr_pcalloc(r->pool, sizeof(modsec_rec)); + if (msr == NULL) return NULL; + + apr_allocator_create(&allocator); + apr_allocator_max_free_set(allocator, 1024); + apr_pool_create_ex(&msr->mp, r->pool, NULL, allocator); + if (msr->mp == NULL) return NULL; + apr_allocator_owner_set(allocator, msr->mp); + + msr->modsecurity = modsecurity; + msr->r = r; + msr->r_early = r; + msr->request_time = r->request_time; + msr->dcfg1 = (directory_config *)ap_get_module_config(r->per_dir_config, + &security2_module); + + /** + * Create a special user configuration. This is where + * explicit instructions will be stored. This will be used + * to override the default settings (and to override the + * configuration in the second phase, dcfg2, with the user + * setting executed in the first phase. + */ + msr->usercfg = create_directory_config(msr->mp, NULL); + if (msr->usercfg == NULL) return NULL; + + /* Create a transaction context and populate + * it using the directory config we just + * got from Apache. + */ + msr->txcfg = create_directory_config(msr->mp, NULL); + if (msr->txcfg == NULL) return NULL; + + if (msr->dcfg1 != NULL) { + msr->txcfg = merge_directory_configs(msr->mp, msr->txcfg, msr->dcfg1); + if (msr->txcfg == NULL) return NULL; + } + init_directory_config(msr->txcfg); + + msr->txid = get_env_var(r, "UNIQUE_ID"); + if (msr->txid == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, r->server, + "ModSecurity: ModSecurity requires mod_unique_id to be installed."); + return NULL; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Initialising transaction (txid %s).", msr->txid); + } + + /* Populate tx fields */ + msr->error_messages = apr_array_make(msr->mp, 5, sizeof(error_message *)); + msr->alerts = apr_array_make(msr->mp, 5, sizeof(char *)); + + msr->server_software = real_server_signature; + msr->local_addr = r->connection->local_ip; + msr->local_port = r->connection->local_addr->port; + + msr->remote_addr = r->connection->remote_ip; + msr->remote_port = r->connection->remote_addr->port; + + msr->request_line = r->the_request; + msr->request_uri = r->uri; + msr->request_method = r->method; + msr->query_string = r->args; + msr->request_protocol = r->protocol; + msr->request_headers = apr_table_copy(msr->mp, r->headers_in); + msr->hostname = ap_get_server_name(r); + + /* Invoke the engine to continue with initialisation */ + if (modsecurity_tx_init(msr) < 0) return NULL; + + store_tx_context(msr, r); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Transaction context created (dcfg %x).", msr->dcfg1); + } + + return msr; +} + + +/* -- Hooks -- */ + +/* + * Change the signature of the server if change was requested in + * configuration. We do this by locating the signature in server + * memory and writing over it. + */ +static apr_status_t change_server_signature(server_rec *s) { + char *server_version = NULL; + + if (new_server_signature == NULL) return 0; + + server_version = (char *)ap_get_server_version(); + if (server_version == NULL) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, s, + "SecServerSignature: Apache returned null as signature."); + return -1; + } + + if (strlen(server_version) >= strlen(new_server_signature)) { + strcpy(server_version, new_server_signature); + } + else { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, s, + "SecServerSignature: original signature too short. Please set " + "ServerTokens to Full."); + return -1; + } + + return 1; +} + +/** + * Executed at the end of server lifetime to cleanup the allocated resources. + */ +static apr_status_t module_cleanup(void *data) { + modsecurity_shutdown(modsecurity); + return APR_SUCCESS; +} + +/** + * Pre-configuration initialisation hook. + */ +static int hook_pre_config(apr_pool_t *mp, apr_pool_t *mp_log, apr_pool_t *mp_temp) { + /* Initialise ModSecurity engine */ + modsecurity = modsecurity_create(mp, MODSEC_ONLINE); + if (modsecurity == NULL) { + /* ENH Since s not available, how do we log from here? stderr? + * ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + * "ModSecurity: Failed to initialise engine."); + */ + return HTTP_INTERNAL_SERVER_ERROR; + } + + return OK; +} + +/** + * Main (post-configuration) module initialisation. + */ +static int hook_post_config(apr_pool_t *mp, apr_pool_t *mp_log, apr_pool_t *mp_temp, server_rec *s) { + void *init_flag = NULL; + int first_time = 0; + + /* Figure out if we are here for the first time */ + apr_pool_userdata_get(&init_flag, "modsecurity-init-flag", s->process->pool); + if (init_flag == NULL) { + first_time = 1; + apr_pool_userdata_set((const void *)1, "modsecurity-init-flag", + apr_pool_cleanup_null, s->process->pool); + } else { + modsecurity_init(modsecurity, mp); + } + + /* Store the original server signature */ + real_server_signature = apr_pstrdup(mp, ap_get_server_version()); + + /* Make some space in the server signature for later */ + if (new_server_signature != NULL) { + ap_add_version_component(mp, new_server_signature); + change_server_signature(s); + } + + #if (!(defined(WIN32) || defined(NETWARE))) + + /* Internal chroot functionality */ + + if (chroot_dir != NULL) { + + /* ENH Is it safe to simply return with an error, instead + * of using exit()? + */ + + if (first_time == 0) { + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity: chroot checkpoint #2 (pid=%i ppid=%i)", getpid(), getppid()); + + if (chdir(chroot_dir) < 0) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, s, + "ModSecurity: chroot failed, unable to chdir to %s, errno=%d (%s)", + chroot_dir, errno, strerror(errno)); + exit(1); + } + + if (chroot(chroot_dir) < 0) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, s, + "ModSecurity: chroot failed, path=%s, errno=%d(%s)", + chroot_dir, errno, strerror(errno)); + exit(1); + } + + if (chdir("/") < 0) { + ap_log_error(APLOG_MARK, APLOG_ERR | APLOG_NOERRNO, 0, s, + "ModSecurity: chdoot failed, unable to chdir to /, errno=%d (%s)", + errno, strerror(errno)); + exit(1); + } + + chroot_completed = 1; + + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity: chroot successful, path=%s", chroot_dir); + } else { + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity: chroot checkpoint #1 (pid=%i ppid=%i)", getpid(), getppid()); + } + } + #endif + + /* Schedule main cleanup for later, when the main pool is destroyed. */ + apr_pool_cleanup_register(mp, (void *)s, module_cleanup, apr_pool_cleanup_null); + + /* Log our presence to the error log. */ + if (first_time) { + if (new_server_signature != NULL) { + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity for Apache %s configured - %s", MODULE_RELEASE, real_server_signature); + } + else { + ap_log_error(APLOG_MARK, APLOG_NOTICE | APLOG_NOERRNO, 0, s, + "ModSecurity for Apache %s configured", MODULE_RELEASE); + } + } + + srand((unsigned int)(time(NULL) * getpid())); + + return OK; +} + +/** + * Initialisation performed for every new child process. + */ +static void hook_child_init(apr_pool_t *mp, server_rec *s) { + modsecurity_child_init(modsecurity); +} + +/** + * Initial request processing, executed immediatelly after + * Apache receives the request headers. + */ +static int hook_request_early(request_rec *r) { + modsec_rec *msr = NULL; + int rc; + + /* This function needs to run only once per transaction + * (i.e. subrequests and redirects are excluded). + */ + if ((r->main != NULL)||(r->prev != NULL)) { + return DECLINED; + } + + /* Initialise transaction context and + * create the initial configuration. + */ + msr = create_tx_context(r); + if (msr == NULL) return DECLINED; + + /* Are we allowed to continue? */ + if (msr->txcfg->is_enabled == MODSEC_DISABLED) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Processing disabled, skipping (hook request_early)."); + } + return DECLINED; + } + + /* Check request body limit (should only trigger on non-chunked requests). */ + if (msr->request_content_length > msr->txcfg->reqbody_limit) { + msr_log(msr, 1, "Request body is larger than the " + "configured limit (%lu).", msr->txcfg->reqbody_limit); + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } + + /* Process phase REQUEST_HEADERS */ + rc = DECLINED; + if (modsecurity_process_phase(msr, PHASE_REQUEST_HEADERS)) { + rc = perform_interception(msr); + } + + return rc; +} + +/** + * Invoked as the first hook in the handler chain, this function + * executes the second phase of ModSecurity request processing. + */ +static int hook_request_late(request_rec *r) { + char *my_error_msg = NULL; + modsec_rec *msr = NULL; + int rc; + + /* Find the transaction context and make sure + * we are supposed to proceed. + */ + msr = retrieve_tx_context(r); + if (msr == NULL) { + /* If we can't find the context that probably means it's + * a subrequest that was not initiated from the outside. + */ + return DECLINED; + } + msr->r = r; + msr->remote_user = r->user; + + /* Has this phase been completed already? */ + if (msr->phase_request_body_complete) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Phase REQUEST_BODY already complete, skipping."); + } + return DECLINED; + } + msr->phase_request_body_complete = 1; + + /* Get the second configuration context. */ + msr->dcfg2 = (directory_config *)ap_get_module_config(r->per_dir_config, + &security2_module); + + /* Create a transaction context. */ + msr->txcfg = create_directory_config(msr->mp, NULL); + if (msr->txcfg == NULL) return DECLINED; + if (msr->dcfg2 != NULL) { + msr->txcfg = merge_directory_configs(msr->mp, msr->txcfg, msr->dcfg2); + if (msr->txcfg == NULL) return DECLINED; + } + + /* Update with the explicit user settings. */ + msr->txcfg = merge_directory_configs(msr->mp, msr->txcfg, msr->usercfg); + + init_directory_config(msr->txcfg); + + if (msr->txcfg->is_enabled == 0) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Processing disabled, skipping (hook request_late)."); + } + return DECLINED; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Second phase starting (dcfg %x).", msr->dcfg2); + } + + /* Figure out whether or not to extract multipart files. */ + if ((msr->txcfg->upload_keep_files != KEEP_FILES_OFF) /* user might want to keep them */ + || (msr->txcfg->upload_validates_files)) /* user might want to validate them */ + { + msr->upload_extract_files = 1; + msr->upload_remove_files = 1; + } + + rc = read_request_body(msr, &my_error_msg); + if (rc < 0) { + switch(rc) { + case -1 : + msr_log(msr, 1, "%s", my_error_msg); + return HTTP_INTERNAL_SERVER_ERROR; + break; + case -4 : /* Timeout. */ + r->connection->keepalive = AP_CONN_CLOSE; + return HTTP_REQUEST_TIME_OUT; + break; + case -5 : /* Request body limit reached. */ + r->connection->keepalive = AP_CONN_CLOSE; + return HTTP_REQUEST_ENTITY_TOO_LARGE; + break; + default : + /* allow through */ + break; + } + + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + } + + /* Update the request headers. They might have changed after + * the body was read (trailers). + */ + // TODO We still need to keep a copy of the original headers + // to log in the audit log. + msr->request_headers = apr_table_copy(msr->mp, r->headers_in); + + /* Process phase REQUEST_BODY */ + record_time_checkpoint(msr, 1); + + rc = DECLINED; + if (modsecurity_process_phase(msr, PHASE_REQUEST_BODY)) { + rc = perform_interception(msr); + } + + record_time_checkpoint(msr, 2); + + return rc; +} + +/** + * Invoked every time Apache has something to write to the error log. + */ +static void hook_error_log(const char *file, int line, int level, apr_status_t status, + const server_rec *s, const request_rec *r, apr_pool_t *mp, const char *fmt) +{ + modsec_rec *msr = NULL; + error_message *em = NULL; + + if (r == NULL) return; + msr = retrieve_tx_context(r); + if (msr == NULL) return; + + /* Store the error message for later */ + em = (error_message *)apr_pcalloc(msr->mp, sizeof(error_message)); + if (em == NULL) return; + + if (file != NULL) em->file = apr_pstrdup(msr->mp, file); + em->line = line; + em->level = level; + em->status = status; + if (fmt != NULL) em->message = apr_pstrdup(msr->mp, fmt); + + /* Remove \n from the end of the message */ + if (em->message != NULL) { + char *p = (char *)em->message; + while(*p != '\0') { + if ((*(p + 1) == '\0')&&(*p == '\n')) { + *p = '\0'; + break; + } + p++; + } + } + + *(const error_message **)apr_array_push(msr->error_messages) = em; +} + +/** + * Guardian logger is used to interface to the external + * script for web server protection - httpd_guardian. + */ +static void sec_guardian_logger(request_rec *r, request_rec *origr, modsec_rec *msr) { + char *str1, *str2, *text; + char *modsec_message = "-"; + int modsec_rating = 0; /* not used yet */ + apr_size_t nbytes, nbytes_written; + apr_time_t duration = (apr_time_now() - origr->request_time); + int limit, was_limited; + + /* bail out if we do not have where to write */ + if ((guardianlog_name == NULL)||(guardianlog_fd == NULL)) return; + + /* process the condition, if we have one */ + if (guardianlog_condition != NULL) { + if (*guardianlog_condition == '!') { + if (apr_table_get(r->subprocess_env, guardianlog_condition + 1) != NULL) { + return; + } + } + else { + if (apr_table_get(r->subprocess_env, guardianlog_condition) == NULL) { + return; + } + } + } + + /* + * Log format is as follows: + * + * %V %h %l %u %t "%r" %>s %b "%{Referer}i" "%{User-agent}i" %{UNIQUE_ID}e + * "SESSION_ID" %T %D "MODSEC_MESSAGE" MODSEC_RATING + * + * The fields SESSION_ID, MODSEC_MESSAGE, and MODSEC_RATING are not used at the moment. + */ + + str2 = apr_psprintf(msr->mp, "%" APR_TIME_T_FMT " %" APR_TIME_T_FMT " \"%s\" %i", + duration, apr_time_sec(duration), log_escape(msr->mp, modsec_message), modsec_rating); + if (str2 == NULL) return; + + /* We do not want the index line to be longer than 3980 bytes. */ + limit = 3980; + was_limited = 0; + + /* If we are logging to a pipe we need to observe and + * obey the pipe atomic write limit - PIPE_BUF. For + * more details see the discussion in sec_guardian_logger, + * above. + */ + if (msr->txcfg->auditlog_name[0] == '|') { + if (PIPE_BUF < limit) { + limit = PIPE_BUF; + } + } + + limit = limit - strlen(str2) - 5; + if (limit <= 0) { + msr_log(msr, 1, "Audit Log: Atomic PIPE write buffer too small: %i", PIPE_BUF); + return; + } + + str1 = construct_log_vcombinedus_limited(msr, limit, &was_limited); + if (str1 == NULL) return; + + if (was_limited == 0) { + text = apr_psprintf(msr->mp, "%s %s \n", str1, str2); + } else { + text = apr_psprintf(msr->mp, "%s %s L\n", str1, str2); + } + if (text == NULL) return; + + nbytes = strlen(text); + apr_file_write_full(guardianlog_fd, text, nbytes, &nbytes_written); +} + +/** + * Invoked at the end of each transaction. + */ +static int hook_log_transaction(request_rec *r) { + const apr_array_header_t *arr = NULL; + request_rec *origr = NULL; + modsec_rec *msr = NULL; + + msr = retrieve_tx_context(r); + if (msr == NULL) { + return DECLINED; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Initialising logging."); + } + + /* Find the first (origr) and the last (r) request */ + origr = r; + while(origr->prev) { + origr = origr->prev; + } + while(r->next) { + r = r->next; + } + + /* At this point r is the last request in the + * chain. However, we now need to detect a case when + * a bad ErrorDocument was used and back out of it. That's + * how Apache does it internally. Except where Apache knows + * exactly what is happening we will have to rely on the missing + * headers in the final request to detect this condition. + */ + arr = apr_table_elts(r->headers_out); + while ((arr->nelts == 0)&&(r->prev != NULL)) { + r = r->prev; + arr = apr_table_elts(r->headers_out); + } + + msr->r = r; + msr->response_status = r->status; + msr->status_line = ((r->status_line != NULL) + ? r->status_line : ap_get_status_line(r->status)); + msr->response_protocol = get_response_protocol(origr); + msr->response_headers = apr_table_copy(msr->mp, r->headers_out); + if (!r->assbackwards) msr->response_headers_sent = 1; + msr->bytes_sent = r->bytes_sent; + msr->local_user = r->user; + msr->remote_user = r->connection->remote_logname; + + /* -- Guardian -- */ + + sec_guardian_logger(r, origr, msr); + + /* Invoke the engine to do the rest of the work now. */ + modsecurity_process_phase(msr, PHASE_LOGGING); + + return DECLINED; +} + +/** + * Invoked right before request processing begins. This is + * when we need to decide if we want to hook into the output + * filter chain. + */ +static void hook_insert_filter(request_rec *r) { + modsec_rec *msr = NULL; + + /* Find the transaction context and make sure we are + * supposed to proceed. + */ + msr = retrieve_tx_context(r); + if (msr == NULL) return; + + if (msr->txcfg->is_enabled == 0) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_filter: Processing disabled, skipping."); + } + return; + } + + if (msr->if_status == IF_STATUS_WANTS_TO_RUN) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_filter: Adding input forwarding filter (r %x).", r); + } + ap_add_input_filter("MODSECURITY_IN", msr, r, r->connection); + } + + /* We always add the output filter because that's where we need to + * initiate our 3rd and 4th processing phases from. The filter is + * smart enough not to buffer the data if it is not supposed to. + */ + if (msr->of_status != OF_STATUS_COMPLETE) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_filter: Adding output filter (r %x).", r); + } + ap_add_output_filter("MODSECURITY_OUT", msr, r, r->connection); + } +} + +#if 0 +/** + * Invoked whenever Apache starts processing an error. A chance + * to insert ourselves into the output filter chain. + */ +static void hook_insert_error_filter(request_rec *r) { + modsec_rec *msr = NULL; + + /* Find the transaction context and make sure we are + * supposed to proceed. + */ + + /* TODO Insert filter but make a note that it's the error + * response the filter would be receiving. + */ + + msr = retrieve_tx_context(r); + if (msr == NULL) return; + + if (msr->txcfg->is_enabled == 0) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_error_filter: Processing disabled, skipping."); + } + return; + } + + if (msr->of_status != OF_STATUS_COMPLETE) { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_error_filter: Adding output filter (r %x).", r); + } + ap_add_output_filter("MODSECURITY_OUT", msr, r, r->connection); + } else { + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Hook insert_error_filter: Output buffering already complete."); + } + } +} +#endif + +/** + * This function is exported for other Apache modules to + * register new transformation functions. + */ +static void modsec_register_tfn(const char *name, void *fn) { + if (modsecurity != NULL) { + msre_engine_tfn_register(modsecurity->msre, name, fn); + } +} + +/** + * This function is exported for other Apache modules to + * register new operators. + */ +static void modsec_register_operator(const char *name, void *fn_init, void *fn_exec) { + if (modsecurity != NULL) { + msre_engine_op_register(modsecurity->msre, name, fn_init, fn_exec); + } +} + +/** + * Registers module hooks with Apache. + */ +static void register_hooks(apr_pool_t *mp) { + static const char *postconfig_beforeme_list[] = { + "mod_unique_id.c", + "mod_ssl.c", + NULL + }; + static const char *postconfig_afterme_list[] = { + "mod_fcgid.c", + "mod_cgid.c", + NULL + }; + static const char *postread_beforeme_list[] = { + "mod_unique_id.c", + NULL + }; + static const char *postread_afterme_list[] = { + "mod_log_forensic.c", + NULL + }; + + /* Export optional functions. */ + APR_REGISTER_OPTIONAL_FN(modsec_register_tfn); + APR_REGISTER_OPTIONAL_FN(modsec_register_operator); + + /* Main hooks */ + ap_hook_pre_config(hook_pre_config, NULL, NULL, APR_HOOK_FIRST); + ap_hook_post_config(hook_post_config, postconfig_beforeme_list, + postconfig_afterme_list, APR_HOOK_REALLY_LAST); + ap_hook_child_init(hook_child_init, NULL, NULL, APR_HOOK_MIDDLE); + + /* Our own hook to handle RPC transactions (not used at the moment). + * // ap_hook_handler(hook_handler, NULL, NULL, APR_HOOK_MIDDLE); + */ + + /* Transaction processing hooks */ + ap_hook_post_read_request(hook_request_early, + postread_beforeme_list, postread_afterme_list, APR_HOOK_REALLY_FIRST); + + ap_hook_fixups(hook_request_late, NULL, NULL, APR_HOOK_REALLY_FIRST); + + /* Logging */ + ap_hook_error_log(hook_error_log, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_log_transaction(hook_log_transaction, NULL, NULL, APR_HOOK_MIDDLE); + + /* Filter hooks */ + ap_hook_insert_filter(hook_insert_filter, NULL, NULL, APR_HOOK_FIRST); + /* ap_hook_insert_error_filter(hook_insert_error_filter, NULL, NULL, APR_HOOK_FIRST); */ + + ap_register_input_filter("MODSECURITY_IN", input_filter, + NULL, AP_FTYPE_CONTENT_SET); + ap_register_output_filter("MODSECURITY_OUT", output_filter, + NULL, AP_FTYPE_CONTENT_SET); +} + +/* Defined in apache2_config.c */ +extern const command_rec module_directives[]; + +/* Module entry points */ +module AP_MODULE_DECLARE_DATA security2_module = { + STANDARD20_MODULE_STUFF, + create_directory_config, + merge_directory_configs, + NULL, /* create_server_config */ + NULL, /* merge_server_configs */ + module_directives, + register_hooks +}; diff --git a/apache2/modsecurity.c b/apache2/modsecurity.c new file mode 100644 index 00000000..a097210e --- /dev/null +++ b/apache2/modsecurity.c @@ -0,0 +1,468 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: modsecurity.c,v 1.7 2006/12/28 10:39:13 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "apr_global_mutex.h" + +#include "modsecurity.h" +#include "msc_parsers.h" +#include "msc_util.h" + +/** + * Log an alert message to the log, adding the rule metadata at the end. + */ +void msc_alert(modsec_rec *msr, int level, msre_actionset *actionset, const char *action_message, + const char *rule_message) +{ + const char *message = NULL; + + if (rule_message == NULL) rule_message = "Unknown error."; + + message = apr_psprintf(msr->mp, "%s %s%s", action_message, + rule_message, msre_format_metadata(msr, actionset)); + + msr_log(msr, level, "%s", message); +} + +#if 0 +/** + * Return phase name associated with the given phase number. + */ +static const char *phase_name(int phase) { + switch(phase) { + case 1 : + return "REQUEST_HEADERS"; + break; + case 2 : + return "REQUEST_BODY"; + break; + case 3 : + return "RESPONSE_HEADERS"; + break; + case 4 : + return "RESPONSE_BODY"; + break; + case 5 : + return "LOGGING"; + break; + } + return "INVALID"; +} +#endif + +/** + * Creates and initialises a ModSecurity engine instance. + */ +msc_engine *modsecurity_create(apr_pool_t *mp, int processing_mode) { + msc_engine *msce = NULL; + + msce = apr_pcalloc(mp, sizeof(msc_engine)); + if (msce == NULL) return NULL; + + msce->mp = mp; + msce->processing_mode = processing_mode; + + msce->msre = msre_engine_create(msce->mp); + if (msce->msre == NULL) return NULL; + msre_engine_register_default_variables(msce->msre); + msre_engine_register_default_operators(msce->msre); + msre_engine_register_default_tfns(msce->msre); + msre_engine_register_default_actions(msce->msre); + + return msce; +} + +/** + * Initialise the modsecurity engine. This function must be invoked + * after configuration processing is complete as Apache needs to know the + * username it is running as. + */ +int modsecurity_init(msc_engine *msce, apr_pool_t *mp) { + apr_status_t rc; + + /* Serial audit log mutext */ + rc = apr_global_mutex_create(&msce->auditlog_lock, NULL, APR_LOCK_DEFAULT, mp); + if (rc != APR_SUCCESS) { + //ap_log_error(APLOG_MARK, APLOG_ERR, rv, s, "mod_security: Could not create modsec_auditlog_lock"); + //return HTTP_INTERNAL_SERVER_ERROR; + return -1; + } + + #ifdef __SET_MUTEX_PERMS + rc = unixd_set_global_mutex_perms(msce->auditlog_lock); + if (rc != APR_SUCCESS) { + // ap_log_error(APLOG_MARK, APLOG_ERR, rc, s, "mod_security: Could not set permissions on modsec_auditlog_lock; check User and Group directives"); + // return HTTP_INTERNAL_SERVER_ERROR; + return -1; + } + #endif + + return 1; +} + +/** + * Performs per-child (new process) initialisation. + */ +void modsecurity_child_init(msc_engine *msce) { + if (msce->auditlog_lock != NULL) { + apr_status_t rc = apr_global_mutex_child_init(&msce->auditlog_lock, NULL, msce->mp); + if (rc != APR_SUCCESS) { + // ap_log_error(APLOG_MARK, APLOG_ERR, rs, s, "Failed to child-init auditlog mutex"); + } + } +} + +/** + * Releases resources held by engine instance. + */ +void modsecurity_shutdown(msc_engine *msce) { + if (msce == NULL) return; +} + +/** + * + */ +static apr_status_t modsecurity_tx_cleanup(void *data) { + modsec_rec *msr = (modsec_rec *)data; + const apr_array_header_t *arr; + apr_table_entry_t *te; + int collect_garbage = 0; + int i; + + if (msr == NULL) return APR_SUCCESS; + + if (rand() < RAND_MAX/100) { + collect_garbage = 1; + } + + /* Collections, store & remove stale. */ + arr = apr_table_elts(msr->collections); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + apr_table_t *col = (apr_table_t *)te[i].val; + + /* Only store those collections that changed. */ + if (apr_table_get(msr->collections_dirty, te[i].key)) { + collection_store(msr, col); + } + + if (collect_garbage) { + collections_remove_stale(msr, te[i].key); + } + } + + /* Multipart processor cleanup. */ + if (msr->mpd != NULL) multipart_cleanup(msr); + + #ifdef WITH_LIBXML2 + /* XML processor cleanup. */ + if (msr->xml != NULL) xml_cleanup(msr); + #endif + + modsecurity_request_body_clear(msr); + + return APR_SUCCESS; +} + +/** + * + */ +apr_status_t modsecurity_tx_init(modsec_rec *msr) { + const char *s = NULL; + const apr_array_header_t *arr; + apr_table_entry_t *te; + int i; + + /* Register TX cleanup */ + apr_pool_cleanup_register(msr->mp, msr, modsecurity_tx_cleanup, apr_pool_cleanup_null); + + /* Initialise C-L */ + msr->request_content_length = -1; + s = apr_table_get(msr->request_headers, "Content-Length"); + if (s != NULL) { + msr->request_content_length = strtol(s, NULL, 10); + } + + /* Figure out whether this request has a body */ + msr->reqbody_chunked = 0; + msr->reqbody_should_exist = 0; + if (msr->request_content_length == -1) { + /* There's no C-L, but is chunked encoding used? */ + char *transfer_encoding = (char *)apr_table_get(msr->request_headers, "Transfer-Encoding"); + if ((transfer_encoding != NULL)&&(strstr(transfer_encoding, "chunked") != NULL)) { + msr->reqbody_should_exist = 1; + msr->reqbody_chunked = 1; + } + } else { + /* C-L found */ + msr->reqbody_should_exist = 1; + } + + /* Initialise C-T */ + msr->request_content_type = NULL; + s = apr_table_get(msr->request_headers, "Content-Type"); + if (s != NULL) msr->request_content_type = s; + + /* Decide what to do with the request body. */ + if ((msr->request_content_type != NULL) + && (strncasecmp(msr->request_content_type, "application/x-www-form-urlencoded", 33) == 0)) + { + /* Always place POST requests with + * "application/x-www-form-urlencoded" payloads in memory. + */ + msr->msc_reqbody_storage = MSC_REQBODY_MEMORY; + msr->msc_reqbody_spilltodisk = 0; + msr->msc_reqbody_processor = "URLENCODED"; + } else { + /* If the C-L is known and there's more data than + * our limit go to disk straight away. + */ + if ((msr->request_content_length != -1) + && (msr->request_content_length > msr->txcfg->reqbody_inmemory_limit)) + { + msr->msc_reqbody_storage = MSC_REQBODY_DISK; + } + + /* In all other cases, try using the memory first + * but switch over to disk for larger bodies. + */ + msr->msc_reqbody_storage = MSC_REQBODY_MEMORY; + msr->msc_reqbody_spilltodisk = 1; + + if (msr->request_content_type != NULL) { + if (strncasecmp(msr->request_content_type, "multipart/form-data", 19) == 0) { + msr->msc_reqbody_processor = "MULTIPART"; + } + } + } + + /* Initialise arguments */ + msr->arguments = apr_table_make(msr->mp, 32); + if (msr->arguments == NULL) return -1; + if (msr->query_string != NULL) { + int invalid_count = 0; + + if (parse_arguments(msr, msr->query_string, msr->txcfg->argument_separator, + "QUERY_STRING", msr->arguments, &invalid_count) < 0) { + msr_log(msr, 1, "Initialisation: Error occurred while parsing QUERY_STRING arguments."); + return -1; + } + } + + msr->arguments_to_sanitise = apr_table_make(msr->mp, 16); + if (msr->arguments_to_sanitise == NULL) return -1; + msr->request_headers_to_sanitise = apr_table_make(msr->mp, 16); + if (msr->request_headers_to_sanitise == NULL) return -1; + msr->response_headers_to_sanitise = apr_table_make(msr->mp, 16); + if (msr->response_headers_to_sanitise == NULL) return -1; + + /* Initialise cookies */ + msr->request_cookies = apr_table_make(msr->mp, 16); + if (msr->request_cookies == NULL) return -1; + + /* Locate the cookie headers and parse them */ + arr = apr_table_elts(msr->request_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + if (strcasecmp(te[i].key, "Cookie") == 0) { + if (msr->txcfg->cookie_format == COOKIES_V0) { + parse_cookies_v0(msr, te[i].val, msr->request_cookies); + } else { + parse_cookies_v1(msr, te[i].val, msr->request_cookies); + } + } + } + + /* Collections. */ + msr->tx_vars = apr_table_make(msr->mp, 32); + if (msr->tx_vars == NULL) return -1; + + msr->collections = apr_table_make(msr->mp, 8); + if (msr->collections == NULL) return -1; + msr->collections_dirty = apr_table_make(msr->mp, 8); + if (msr->collections_dirty == NULL) return -1; + + return 1; +} + +/** + * + */ +static int is_response_status_relevant(modsec_rec *msr, int status) { + char *my_error_msg = NULL; + apr_status_t rc; + char buf[32]; + + if ((msr->txcfg->auditlog_relevant_regex == NULL) + ||(msr->txcfg->auditlog_relevant_regex == NOT_SET_P)) + { + return 0; + } + + apr_snprintf(buf, sizeof(buf), "%i", status); + + rc = msc_regexec(msr->txcfg->auditlog_relevant_regex, buf, strlen(buf), &my_error_msg); + if (rc >= 0) return 1; + if (rc == PCRE_ERROR_NOMATCH) return 0; + + msr_log(msr, 1, "Regex processing failed (rc %i): %s", rc, my_error_msg); + return 0; +} + +/** + * + */ +static apr_status_t modsecurity_process_phase_request_headers(modsec_rec *msr) { + msr_log(msr, 4, "Starting phase REQUEST_HEADERS."); + + if (msr->txcfg->ruleset != NULL) { + return msre_ruleset_process_phase(msr->txcfg->ruleset, msr); + } + + return 0; +} + +/** + * + */ +static apr_status_t modsecurity_process_phase_request_body(modsec_rec *msr) { + msr_log(msr, 4, "Starting phase REQUEST_BODY."); + + if (msr->txcfg->ruleset != NULL) { + return msre_ruleset_process_phase(msr->txcfg->ruleset, msr); + } + + return 0; +} + +/** + * + */ +static apr_status_t modsecurity_process_phase_response_headers(modsec_rec *msr) { + msr_log(msr, 4, "Starting phase RESPONSE_HEADERS."); + + if (msr->txcfg->ruleset != NULL) { + return msre_ruleset_process_phase(msr->txcfg->ruleset, msr); + } + + return 0; +} + +/** + * + */ +static apr_status_t modsecurity_process_phase_response_body(modsec_rec *msr) { + msr_log(msr, 4, "Starting phase RESPONSE_BODY."); + + if (msr->txcfg->ruleset != NULL) { + return msre_ruleset_process_phase(msr->txcfg->ruleset, msr); + } + + return 0; +} + +/** + * + */ +static apr_status_t modsecurity_process_phase_logging(modsec_rec *msr) { + msr_log(msr, 4, "Starting phase LOGGING."); + + if (msr->txcfg->ruleset != NULL) { + msre_ruleset_process_phase(msr->txcfg->ruleset, msr); + } + + /* Is this request relevant for logging purposes? */ + if (msr->is_relevant == 0) { + /* Check the status */ + msr->is_relevant += is_response_status_relevant(msr, msr->r->status); + + /* If we processed two requests and statuses are different then + * check the other status too. + */ + if (msr->r_early->status != msr->r->status) { + msr->is_relevant += is_response_status_relevant(msr, msr->r_early->status); + } + } + + /* Figure out if we want to keep the files (if there are any, of course). */ + if ((msr->txcfg->upload_keep_files == KEEP_FILES_ON) + || ((msr->txcfg->upload_keep_files == KEEP_FILES_RELEVANT_ONLY)&&(msr->is_relevant))) + { + msr->upload_remove_files = 0; + } else { + msr->upload_remove_files = 1; + } + + /* Are we configured for audit logging? */ + switch(msr->txcfg->auditlog_flag) { + case AUDITLOG_OFF : + msr_log(msr, 4, "Audit log: Not configured to run for this request."); + return DECLINED; + break; + + case AUDITLOG_RELEVANT : + if (msr->is_relevant == 0) { + msr_log(msr, 4, "Audit log: Ignoring a non-relevant request."); + return DECLINED; + } + break; + + case AUDITLOG_ON : + /* All right, do nothing */ + break; + + default : + return HTTP_INTERNAL_SERVER_ERROR; + break; + } + + /* Invoke the Audit logger */ + msr_log(msr, 4, "Audit log: Logging this transaction."); + + sec_audit_logger(msr); + + return 0; +} + +/** + * Processes one transaction phase. The phase number does not + * need to be explicitly provided since it's already available + * in the modsec_rec structure. + */ +apr_status_t modsecurity_process_phase(modsec_rec *msr, int phase) { + msr->phase = phase; + + switch(phase) { + case 1 : + return modsecurity_process_phase_request_headers(msr); + break; + case 2 : + return modsecurity_process_phase_request_body(msr); + break; + case 3 : + return modsecurity_process_phase_response_headers(msr); + break; + case 4 : + return modsecurity_process_phase_response_body(msr); + break; + case 5 : + return modsecurity_process_phase_logging(msr); + break; + default : + msr_log(msr, 1, "Invalid processing phase: %i", msr->phase); + return -1; + break; + } + + return 0; +} diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h new file mode 100644 index 00000000..9cbc5f3e --- /dev/null +++ b/apache2/modsecurity.h @@ -0,0 +1,469 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: modsecurity.h,v 1.27 2007/02/05 12:44:40 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MODSECURITY_H_ +#define _MODSECURITY_H_ + +#include +#include +#include + +typedef struct rule_exception rule_exception; +typedef struct modsec_rec modsec_rec; +typedef struct directory_config directory_config; +typedef struct error_message error_message; +typedef struct msc_engine msc_engine; +typedef struct msc_data_chunk msc_data_chunk; +typedef struct msc_arg msc_arg; +typedef struct msc_string msc_string; + +#ifndef WIN32 +#define DSOLOCAL __attribute__((visibility("hidden"))) +#else +#define DSOLOCAL +#endif + +#include "msc_logging.h" +#include "msc_multipart.h" +#include "msc_pcre.h" +#include "msc_util.h" +#ifdef WITH_LIBXML2 +#include "msc_xml.h" +#endif +#include "re.h" + +#include "ap_config.h" +#include "apr_md5.h" +#include "apr_strings.h" +#include "httpd.h" +#include "http_config.h" +#include "http_log.h" +#include "http_protocol.h" + +#define MODULE_NAME "ModSecurity" +#define MODULE_RELEASE "2.1.0-rc7" +#define MODULE_NAME_FULL (MODULE_NAME " v" MODULE_RELEASE " (Apache 2.x)") + +#define PHASE_REQUEST_HEADERS 1 +#define PHASE_REQUEST_BODY 2 +#define PHASE_RESPONSE_HEADERS 3 +#define PHASE_RESPONSE_BODY 4 +#define PHASE_LOGGING 5 + +#define NOT_SET -1 +#define NOT_SET_P (void *)-1 + +#define CREATEMODE ( APR_UREAD | APR_UWRITE | APR_GREAD ) +#define CREATEMODE_DIR ( APR_UREAD | APR_UWRITE | APR_UEXECUTE | APR_GREAD | APR_GEXECUTE ) + +#if defined(NETWARE) +#define CREATEMODE_UNISTD ( S_IREAD | S_IWRITE ) +#elif defined(WIN32) +#define CREATEMODE_UNISTD ( _S_IREAD | _S_IWRITE ) +#else +#define CREATEMODE_UNISTD ( S_IRUSR | S_IWUSR | S_IRGRP ) +#endif + +#if !defined(O_BINARY) +#define O_BINARY (0) +#endif + +#ifndef PIPE_BUF +#define PIPE_BUF (512) +#endif + +#define REQUEST_BODY_HARD_LIMIT 1073741824L +#define REQUEST_BODY_DEFAULT_INMEMORY_LIMIT 131072 +#define REQUEST_BODY_DEFAULT_LIMIT 134217728 +#define RESPONSE_BODY_DEFAULT_LIMIT 524288 +#define RESPONSE_BODY_HARD_LIMIT 1073741824L + +#if !defined(OS2) && !defined(WIN32) && !defined(BEOS) && !defined(NETWARE) +#include "unixd.h" +#define __SET_MUTEX_PERMS +#endif + +#define COOKIES_V0 0 +#define COOKIES_V1 1 + +#ifdef WIN32 +#include +#else +#include +#include +#endif + +#define NOTE_MSR "modsecurity-tx-context" + +#define FATAL_ERROR "ModSecurity: Fatal error (memory allocation or unexpected internal error)!" + +extern DSOLOCAL char *new_server_signature; +extern DSOLOCAL char *real_server_signature; +extern DSOLOCAL char *chroot_dir; + +extern module AP_MODULE_DECLARE_DATA security2_module; + +extern DSOLOCAL const command_rec module_directives[]; + +#define RESBODY_STATUS_NOT_READ 0 /* we were not configured to read the body */ +#define RESBODY_STATUS_ERROR 1 /* error occured while we were reading the body */ +#define RESBODY_STATUS_PARTIAL 2 /* partial body content available in the brigade */ +#define RESBODY_STATUS_READ_BRIGADE 3 /* body was read but not flattened */ +#define RESBODY_STATUS_READ 4 /* body was read and flattened */ + +#define IF_STATUS_NONE 0 +#define IF_STATUS_WANTS_TO_RUN 1 +#define IF_STATUS_COMPLETE 2 + +#define OF_STATUS_NOT_STARTED 0 +#define OF_STATUS_IN_PROGRESS 1 +#define OF_STATUS_COMPLETE 2 + +#define MSC_REQBODY_NONE 0 +#define MSC_REQBODY_MEMORY 1 +#define MSC_REQBODY_DISK 2 + +#define ACTION_NONE 0 +#define ACTION_DENY 1 +#define ACTION_REDIRECT 2 +#define ACTION_PROXY 3 +#define ACTION_DROP 4 +#define ACTION_ALLOW 5 + +#define MODSEC_DISABLED 0 +#define MODSEC_DETECTION_ONLY 1 +#define MODSEC_ENABLED 2 + +#define MODSEC_OFFLINE 0 +#define MODSEC_ONLINE 1 + +#define REGEX_CAPTURE_BUFLEN 1024 + +#define KEEP_FILES_OFF 0 +#define KEEP_FILES_ON 1 +#define KEEP_FILES_RELEVANT_ONLY 2 + +#define RULE_EXCEPTION_IMPORT_ID 1 +#define RULE_EXCEPTION_IMPORT_MSG 2 +#define RULE_EXCEPTION_REMOVE_ID 3 +#define RULE_EXCEPTION_REMOVE_MSG 4 + +#define NBSP 160 + +struct rule_exception { + int type; + const char *param; + void *param_data; +}; + +struct modsec_rec { + apr_pool_t *mp; + msc_engine *modsecurity; + + request_rec *r_early; + request_rec *r; + directory_config *dcfg1; + directory_config *dcfg2; + directory_config *usercfg; + directory_config *txcfg; + + unsigned int reqbody_should_exist; + unsigned int reqbody_chunked; + + unsigned int phase; + unsigned int phase_request_headers_complete; + unsigned int phase_request_body_complete; + + apr_bucket_brigade *if_brigade; + unsigned int if_status; + unsigned int if_started_forwarding; + + apr_size_t reqbody_length; + unsigned int reqbody_status; + + apr_bucket_brigade *of_brigade; + unsigned int of_status; + unsigned int of_done_reading; + unsigned int of_skipping; + + unsigned int resbody_status; + apr_size_t resbody_length; + char *resbody_data; + unsigned int resbody_contains_html; + + apr_array_header_t *error_messages; + apr_array_header_t *alerts; + + const char *txid; + const char *sessionid; + const char *userid; + + const char *server_software; + const char *local_addr; + unsigned int local_port; + const char *local_user; + + /* client */ + + const char *remote_addr; + unsigned int remote_port; + const char *remote_user; + + /* request */ + + const char *request_line; + const char *request_method; + const char *request_uri; + const char *query_string; + const char *request_protocol; + + const char *hostname; + + apr_table_t *request_headers; + + apr_off_t request_content_length; + const char *request_content_type; + + apr_table_t *arguments; + apr_table_t *arguments_to_sanitise; + apr_table_t *request_headers_to_sanitise; + apr_table_t *response_headers_to_sanitise; + apr_table_t *request_cookies; + + unsigned int is_relevant; + + apr_table_t *tx_vars; + + /* response */ + unsigned int response_status; + const char *status_line; + const char *response_protocol; + apr_table_t *response_headers; + unsigned int response_headers_sent; + apr_off_t bytes_sent; + + /* modsecurity request body processing stuff */ + + unsigned int msc_reqbody_storage; /* on disk or in memory */ + unsigned int msc_reqbody_spilltodisk; + unsigned int msc_reqbody_read; + + apr_pool_t *msc_reqbody_mp; /* this is where chunks are allocated from */ + apr_array_header_t *msc_reqbody_chunks; /* data chunks when stored in memory */ + unsigned int msc_reqbody_length; /* the amount of data received */ + int msc_reqbody_chunk_position; /* used when retrieving the body */ + unsigned int msc_reqbody_chunk_offset; /* offset of the chunk currently in use */ + msc_data_chunk *msc_reqbody_chunk_current; /* current chunk */ + char *msc_reqbody_buffer; + + const char *msc_reqbody_filename; /* when stored on disk */ + int msc_reqbody_fd; + msc_data_chunk *msc_reqbody_disk_chunk; + + const char *msc_reqbody_processor; + int msc_reqbody_error; + const char *msc_reqbody_error_msg; + + multipart_data *mpd; /* MULTIPART processor data structure */ + + #ifdef WITH_LIBXML2 + xml_data *xml; /* XML processor data structure */ + #endif + + /* audit logging */ + char *new_auditlog_boundary; + char *new_auditlog_filename; + apr_file_t *new_auditlog_fd; + unsigned int new_auditlog_size; + apr_md5_ctx_t new_auditlog_md5ctx; + + unsigned int was_intercepted; + unsigned int intercept_phase; + msre_actionset *intercept_actionset; + const char *intercept_message; + + /* performance measurement */ + apr_time_t request_time; + apr_time_t time_checkpoint_1; + apr_time_t time_checkpoint_2; + apr_time_t time_checkpoint_3; + + const char *matched_var; + + /* upload */ + int upload_extract_files; + int upload_remove_files; + + /* other */ + apr_table_t *collections; + apr_table_t *collections_dirty; +}; + +struct directory_config { + apr_pool_t *mp; + + msre_ruleset *ruleset; + + int is_enabled; + int reqbody_access; + long int reqbody_inmemory_limit; + long int reqbody_limit; + int resbody_access; + + long int of_limit; + apr_table_t *of_mime_types; + int of_mime_types_cleared; + + const char *debuglog_name; + int debuglog_level; + apr_file_t *debuglog_fd; + + int cookie_format; + int argument_separator; + + int rule_inheritance; + apr_array_header_t *rule_exceptions; + + + /* -- Audit log -- */ + + /* Whether audit log should be enabled in the context or not */ + int auditlog_flag; + + /* AUDITLOG_SERIAL (single file) or AUDITLOG_CONCURRENT (multiple files) */ + int auditlog_type; + + /* The name of the audit log file (for the old type), or the + * name of the index file (for the new audit log type) + */ + char *auditlog_name; + + /* The file descriptor for the file above */ + apr_file_t *auditlog_fd; + + /* For the new-style audit log only, the path where + * audit log entries will be stored + */ + char *auditlog_storage_dir; + + /* A list of parts to include in the new-style audit log + * entry. By default, it contains 'ABCFHZ'. Have a look at + * the AUDITLOG_PART_* constants above to decipher the + * meaning. + */ + char *auditlog_parts; + + /* A regular expression that determines if a response + * status is treated as relevant. + */ + msc_regex_t *auditlog_relevant_regex; + + /* Upload */ + const char *tmp_dir; + const char *upload_dir; + int upload_keep_files; + int upload_validates_files; + + /* Used only in the configuration phase. */ + msre_rule *tmp_chain_starter; + msre_actionset *tmp_default_actionset; + + /* Misc */ + const char *data_dir; + const char *webappid; +}; + +struct error_message { + const char *file; + int line; + int level; + apr_status_t status; + const char *message; +}; + +struct msc_engine { + apr_pool_t *mp; + apr_global_mutex_t *auditlog_lock; + msre_engine *msre; + unsigned int processing_mode; +}; + +struct msc_data_chunk { + char *data; + apr_size_t length; + unsigned int is_permanent; +}; + +struct msc_arg { + const char *name; + unsigned int name_len; + unsigned int name_origin_offset; + unsigned int name_origin_len; + const char *value; + unsigned int value_len; + unsigned int value_origin_offset; + unsigned int value_origin_len; + const char *origin; +}; + +struct msc_string { + char *name; + unsigned int name_len; + char *value; + unsigned int value_len; +}; + + +/* Engine functions */ + +msc_engine DSOLOCAL *modsecurity_create(apr_pool_t *mp, int processing_mode); + +int DSOLOCAL modsecurity_init(msc_engine *msce, apr_pool_t *mp); + +void DSOLOCAL modsecurity_child_init(msc_engine *msce); + +void DSOLOCAL modsecurity_shutdown(msc_engine *msce); + +apr_status_t DSOLOCAL modsecurity_tx_init(modsec_rec *msr); + +apr_status_t DSOLOCAL modsecurity_process_phase(modsec_rec *msr, int phase); + + +/* Request body functions */ + +apr_status_t DSOLOCAL modsecurity_request_body_start(modsec_rec *msr); + +apr_status_t DSOLOCAL modsecurity_request_body_store(modsec_rec *msr, + const char *data, apr_size_t length); + +apr_status_t DSOLOCAL modsecurity_request_body_end(modsec_rec *msr); + +apr_status_t DSOLOCAL modsecurity_request_body_retrieve_start(modsec_rec *msr); + +apr_status_t DSOLOCAL modsecurity_request_body_retrieve_end(modsec_rec *msr); + +/* Retrieves up to nbytes bytes of the request body. Returns 1 on + * success, 0 when there is no more data, or -1 on error. On return + * nbytes will contain the number of bytes stored in the buffer. + */ +apr_status_t DSOLOCAL modsecurity_request_body_retrieve(modsec_rec *msr, msc_data_chunk **chunk, + long int nbytes); + +void DSOLOCAL msc_add(modsec_rec *msr, int level, msre_actionset *actionset, + const char *action_message, const char *rule_message); + +void DSOLOCAL msc_alert(modsec_rec *msr, int level, msre_actionset *actionset, const char *action_message, + const char *rule_message); + +apr_status_t DSOLOCAL modsecurity_request_body_clear(modsec_rec *msr); + +#endif diff --git a/apache2/modules.mk b/apache2/modules.mk new file mode 100644 index 00000000..60707cee --- /dev/null +++ b/apache2/modules.mk @@ -0,0 +1,19 @@ + +MOD_SECURITY2 = mod_security2 apache2_config apache2_io apache2_util \ + re re_operators re_actions re_tfns re_variables \ + msc_logging msc_xml msc_multipart modsecurity msc_parsers msc_util msc_pcre \ + persist_dbm msc_reqbody + +H = re.h modsecurity.h msc_logging.h msc_multipart.h msc_parsers.h \ + msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h + +${MOD_SECURITY2:=.slo}: ${H} +${MOD_SECURITY2:=.lo}: ${H} +${MOD_SECURITY2:=.o}: ${H} + +mod_security2.la: ${MOD_SECURITY2:=.slo} + $(SH_LINK) -rpath $(libexecdir) -module -avoid-version ${MOD_SECURITY2:=.lo} + +DISTCLEAN_TARGETS = modules.mk + +shared = mod_security2.la diff --git a/apache2/msc_logging.c b/apache2/msc_logging.c new file mode 100644 index 00000000..4f9c73ec --- /dev/null +++ b/apache2/msc_logging.c @@ -0,0 +1,890 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_logging.c,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "msc_logging.h" +#include "httpd.h" +#include "apr_strings.h" +#include "apr_global_mutex.h" +#include "msc_util.h" + + +/** + * Write the supplied data to the audit log (if the FD is ready), update + * the size counters, update the hash context. + */ +static int sec_auditlog_write(modsec_rec *msr, const char *data, unsigned int len) { + apr_size_t nbytes_written, nbytes = len; + apr_status_t rc; + + if ((msr->new_auditlog_fd == NULL)||(data == NULL)) return -1; + + rc = apr_file_write_full(msr->new_auditlog_fd, data, nbytes, &nbytes_written); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Audit log: Failed writing (requested %ui bytes, written %ui)", + nbytes, nbytes_written); + return -1; + } + + /* Note the following will only take into account the actual + * amount of bytes we've written. + */ + msr->new_auditlog_size += nbytes_written; + apr_md5_update(&msr->new_auditlog_md5ctx, data, nbytes_written); + + return rc; +} + +/** + * Construct a log line in the vcombinedus format (see below). + */ +char *construct_log_vcombinedus(modsec_rec *msr) { + const char *local_user, *remote_user; + const char *referer, *user_agent, *uniqueid; + const char *sessionid; + + /* remote log name */ + if (msr->remote_user == NULL) remote_user = "-"; + else remote_user = msr->remote_user; + + /* authenticated user */ + if (msr->local_user == NULL) local_user = "-"; + else local_user = msr->local_user; + + /* unique id */ + uniqueid = msr->txid; + if (uniqueid == NULL) uniqueid = "-"; + + /* referer */ + referer = "-"; + /* Logging Referer is a waste of space. + referer = (char *)apr_table_get(msr->request_headers, "Referer"); + if (referer == NULL) referer = "-"; + */ + + /* user agent */ + user_agent = "-"; + /* Logging User-Agent is a waste of space too. + user_agent = (char *)apr_table_get(msr->request_headers, "User-Agent"); + if (user_agent == NULL) user_agent = "-"; + */ + + /* sessionid */ + sessionid = (msr->sessionid == NULL ? "-" : msr->sessionid); + + return apr_psprintf(msr->mp, "%s %s %s %s [%s] \"%s\" %i %" APR_OFF_T_FMT " \"%s\" \"%s\" %s \"%s\"", + log_escape_nq(msr->mp, msr->hostname), msr->remote_addr, log_escape_nq(msr->mp, remote_user), + log_escape_nq(msr->mp, local_user), current_logtime(msr->mp), + ((msr->request_line == NULL) ? "" : log_escape(msr->mp, msr->request_line)), + msr->response_status, msr->bytes_sent, log_escape(msr->mp, referer), + log_escape(msr->mp, user_agent), log_escape(msr->mp, uniqueid), sessionid); +} + +/** + * Constructs a log line in vcombined log format trying to truncate + * some of the fields to make the log line shorter than _limit bytes. + */ +char *construct_log_vcombinedus_limited(modsec_rec *msr, int _limit, int *was_limited) { + char *hostname; + char *local_user, *remote_user; + char *referer, *user_agent, *uniqueid; + const char *sessionid; + char *the_request, *bytes_sent; + int limit = _limit; + + /* hostname */ + hostname = (msr->hostname == NULL ? "-" : log_escape_nq(msr->mp, msr->hostname)); + + /* remote log name */ + if (msr->remote_user == NULL) remote_user = "-"; + else remote_user = log_escape_nq(msr->mp, msr->remote_user); + + /* authenticated user */ + if (msr->local_user == NULL) local_user = "-"; + else local_user = log_escape_nq(msr->mp, msr->local_user); + + /* unique id */ + if (msr->txid == NULL) uniqueid = "-"; + else uniqueid = log_escape(msr->mp, msr->txid); + + /* referer */ + referer = "-"; + /* + referer = (char *)apr_table_get(msr->request_headers, "Referer"); + if (referer == NULL) referer = "-"; + else referer = log_escape(msr->mp, referer); + */ + + /* user agent */ + user_agent = "-"; + /* + user_agent = (char *)apr_table_get(msr->request_headers, "User-Agent"); + if (user_agent == NULL) user_agent = "-"; + else user_agent = log_escape(msr->mp, user_agent); + */ + + /* sessionid */ + sessionid = (msr->sessionid == NULL) ? "-" : log_escape(msr->mp, msr->sessionid); + + the_request = (msr->request_line == NULL) ? "" : log_escape(msr->mp, msr->request_line); + + bytes_sent = apr_psprintf(msr->mp, "%" APR_OFF_T_FMT, msr->bytes_sent); + + /* first take away the size of the + * information we must log + */ + limit -= 22; /* spaces and double quotes */ + limit -= strlen(hostname); /* server name or IP */ + limit -= strlen(msr->remote_addr); /* remote IP */ + limit -= 28; /* current_logtime */ + limit -= 3; /* status */ + limit -= strlen(bytes_sent); /* bytes sent */ + limit -= strlen(uniqueid); /* unique id */ + limit -= strlen(sessionid); /* session id */ + + if (limit <= 0) { + msr_log(msr, 1, "GuardianLog: Atomic pipe write size too small: %i", PIPE_BUF); + return NULL; + } + + /* we hope to be able to squeeze everything in */ + if (limit < (int)(strlen(remote_user) + strlen(local_user) + strlen(referer) + + strlen(user_agent) + strlen(the_request))) + { + /* Boo hoo hoo, there's not enough space available. */ + *was_limited = 1; + + /* Let's see if we can reduce the size of something. This + * is a very crude approach but it seems to work for our + * needs. + */ + if (strlen(remote_user) > 32) { + msr_log(msr, 9, "GuardianLog: Reduced remote_user to 32."); + remote_user[32] = '\0'; + } + limit -= strlen(remote_user); + + if (strlen(local_user) > 32) { + msr_log(msr, 9, "GuardianLog: Reduced local_user to 32."); + local_user[32] = '\0'; + } + limit -= strlen(local_user); + + if (strlen(referer) > 64) { + msr_log(msr, 9, "GuardianLog: Reduced referer to 64."); + referer[64] = '\0'; + } + limit -= strlen(referer); + + if (strlen(user_agent) > 64) { + msr_log(msr, 9, "GuardianLog: Reduced user_agent to 64."); + user_agent[64] = '\0'; + } + limit -= strlen(user_agent); + + if (limit <= 0) { + msr_log(msr, 1, "GuardianLog: Atomic pipe write size too small: %i.", PIPE_BUF); + return NULL; + } + + /* use what's left for the request line */ + if ((int)strlen(the_request) > limit) { + the_request[limit] = '\0'; + msr_log(msr, 9, "GuardianLog: Reduced the_request to %i bytes.", limit); + } + } else { + /* Yay! We have enough space! */ + *was_limited = 0; + } + + return apr_psprintf(msr->mp, "%s %s %s %s [%s] \"%s\" %i %s \"%s\" \"%s\" %s \"%s\"", + hostname, msr->remote_addr, remote_user, + local_user, current_logtime(msr->mp), the_request, + msr->response_status, bytes_sent, referer, user_agent, + uniqueid, sessionid + ); +} + +/** + * Checks if the provided string is a valid audit log parts specification. + */ +int is_valid_parts_specification(char *p) { + char c, *t = p; + + while((c = *t++) != '\0') { + if ((c != AUDITLOG_PART_ENDMARKER)&&((c < AUDITLOG_PART_FIRST)||(c > AUDITLOG_PART_LAST))) { + return 0; + } + } + + return 1; +} + +/** + * Constructs a filename that will be used to store an + * audit log entry. + */ +static char *construct_auditlog_filename(apr_pool_t *mp, const char *uniqueid) { + apr_time_exp_t t; + char tstr[300]; + apr_size_t len; + + apr_time_exp_lt(&t, apr_time_now()); + + apr_strftime(tstr, &len, 299, "/%Y%m%d/%Y%m%d-%H%M/%Y%m%d-%H%M%S", &t); + return apr_psprintf(mp, "%s-%s", tstr, uniqueid); +} + +/** + * Creates a random 8-character string that + * consists of hexadecimal numbers, to be used + * as an audit log boundary. + */ +static char *create_auditlog_boundary(request_rec *r) { + unsigned long data = rand(); + /* Do note that I tried using apr_generate_random_bytes but it turned + * out to be terribly slow for some reason. Needs further investigation. + */ + return bytes2hex(r->pool, (void *)&data, 4); +} + +/** + * Sanitises the request line by removing the parameters + * that have been marked as sensitive. + */ +static void sanitise_request_line(modsec_rec *msr) { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + char *qspos; + + /* Locate the query string. */ + qspos = strstr(msr->request_line, "?"); + if (qspos == NULL) return; + qspos++; + + /* Loop through the list of sensitive parameters. */ + tarr = apr_table_elts(msr->arguments_to_sanitise); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msc_arg *arg = (msc_arg *)telts[i].val; + /* Only look at the parameters that appeared in the query string. */ + if (strcmp(arg->origin, "QUERY_STRING") == 0) { + char *p; + int j; + + /* Go to the beginning of the parameter. */ + p = qspos; + j = arg->value_origin_offset; + while((*p != '\0')&&(j--)) p++; + if (*p == '\0') { + msr_log(msr, 1, "Unable to sanitise variable \"%s\" at offset %i of QUERY_STRING" + "because the request line is too short.", + log_escape_ex(msr->mp, arg->name, arg->name_len), + arg->value_origin_offset); + continue; + } + + /* Write over the value. */ + j = arg->value_origin_len; + while((*p != '\0')&&(j--)) { + *p++ = '*'; + } + if (*p == '\0') { + msr_log(msr, 1, "Unable to sanitise variable \"%s\" at offset %i (size %i) " + "of QUERY_STRING because the request line is too short.", + log_escape_ex(msr->mp, arg->name, arg->name_len), + arg->value_origin_offset, arg->value_origin_len); + continue; + } + } + } +} + +/** + * Produce an audit log entry. + */ +void sec_audit_logger(modsec_rec *msr) { + const apr_array_header_t *arr = NULL; + apr_table_entry_t *te = NULL; + char *str1 = NULL, *str2 = NULL, *text = NULL; + apr_size_t nbytes, nbytes_written; + unsigned char md5hash[APR_MD5_DIGESTSIZE]; + int was_limited = 0; + int wrote_response_body = 0; + char *entry_filename, *entry_basename; + apr_status_t rc; + int i, limit; + + /* the boundary is used by both audit log types */ + msr->new_auditlog_boundary = create_auditlog_boundary(msr->r); + + /* Return silently if we don't have a request line. This + * means we will not be logging request timeouts. + */ + if (msr->request_line == NULL) { + msr_log(msr, 4, "Audit log: Skipping request whose request_line is null."); + return; + } + + /* Also return silently if we don't have a file descriptor. */ + if (msr->txcfg->auditlog_fd == NULL) { + msr_log(msr, 4, "Audit log: Skipping request since there is nowhere to write to."); + return; + } + + if (msr->txcfg->auditlog_type != AUDITLOG_CONCURRENT) { + /* Serial logging - we already have an open file + * descriptor to write to. + */ + msr->new_auditlog_fd = msr->txcfg->auditlog_fd; + } else { + /* Concurrent logging - we need to create a brand + * new file for this request. + */ + apr_md5_init(&msr->new_auditlog_md5ctx); + + msr->new_auditlog_filename = construct_auditlog_filename(msr->mp, msr->txid); + if (msr->new_auditlog_filename == NULL) return; + + /* The audit log storage directory should be explicitly + * defined. But if it isn't try to write to the same + * directory where the index file is placed. Of course, + * it is *very* bad practice to allow the Apache user + * to write to the same directory where a root user is + * writing to but it's not us that's causing the problem + * and there isn't anything we can do about that. + * + * TODO Actually there is something we can do! We will make + * SecAuditStorageDir mandatory, ask the user to explicitly + * define the storage location *and* refuse to work if the + * index and the storage location are in the same folder. + */ + if (msr->txcfg->auditlog_storage_dir == NULL) { + entry_filename = file_dirname(msr->mp, msr->txcfg->auditlog_name); + } + else { + entry_filename = msr->txcfg->auditlog_storage_dir; + } + if (entry_filename == NULL) return; + + entry_filename = apr_psprintf(msr->mp, "%s%s", entry_filename, msr->new_auditlog_filename); + if (entry_filename == NULL) return; + entry_basename = file_dirname(msr->mp, entry_filename); + if (entry_basename == NULL) return; + + /* IMP1 Surely it would be more efficient to check the folders for + * the audit log repository base path in the configuration phase, to reduce + * the work we do on every request. Also, since our path depends on time, + * we could cache the time we last checked and don't check if we know + * the folder is there. + */ + rc = apr_dir_make_recursive(entry_basename, CREATEMODE_DIR, msr->mp); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Audit log: Failed to create subdirectories: %s (%s)", + entry_basename, get_apr_error(msr->mp, rc)); + return; + } + + rc = apr_file_open(&msr->new_auditlog_fd, entry_filename, + APR_WRITE | APR_TRUNCATE | APR_CREATE | APR_BINARY | APR_FILE_NOCLEANUP, + CREATEMODE, msr->mp); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Audit log: Failed to create file: %s (%s)", + entry_filename, get_apr_error(msr->mp, rc)); + return; + } + } + + /* Lock the mutex, but only if we are using serial format. */ + if (msr->txcfg->auditlog_type != AUDITLOG_CONCURRENT) { + rc = apr_global_mutex_lock(msr->modsecurity->auditlog_lock); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Audit log: Failed to lock global mutex: %s", + get_apr_error(msr->mp, rc)); + } + } + + /* AUDITLOG_PART_HEADER */ + + text = apr_psprintf(msr->mp, "--%s-A--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + /* Format: time transaction_id remote_addr remote_port local_addr local_port */ + + text = apr_psprintf(msr->mp, "[%s] %s %s %i %s %i", + current_logtime(msr->mp), msr->txid, msr->remote_addr, msr->remote_port, + msr->local_addr, msr->local_port); + sec_auditlog_write(msr, text, strlen(text)); + + + /* AUDITLOG_PART_REQUEST_HEADERS */ + + if (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_REQUEST_HEADERS) != NULL) { + text = apr_psprintf(msr->mp, "\n--%s-B--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + sanitise_request_line(msr); + + sec_auditlog_write(msr, msr->request_line, strlen(msr->request_line)); + sec_auditlog_write(msr, "\n", 1); + + arr = apr_table_elts(msr->request_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + text = apr_psprintf(msr->mp, "%s: %s\n", te[i].key, te[i].val); + /* Do we need to sanitise this request header? */ + if (apr_table_get(msr->request_headers_to_sanitise, te[i].key) != NULL) { + /* Yes, sanitise it. */ + memset(text + strlen(te[i].key) + 2, '*', strlen(te[i].val)); + } + sec_auditlog_write(msr, text, strlen(text)); + } + } + + /* AUDITLOG_PART_REQUEST_BODY */ + + /* Output this part of it was explicitly requested (C) or if it was the faked + * request body that was requested (I) but we have no reason to fake it (it's + * already in the correct format). + */ + if ( (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_REQUEST_BODY) != NULL) + || ( (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_FAKE_REQUEST_BODY) != NULL) + && (msr->mpd == NULL) ) ) + { + if (msr->msc_reqbody_read) { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + apr_array_header_t *sorted_args; + unsigned int offset = 0, last_offset = 0; + msc_arg *nextarg = NULL; + int sanitise = 0; /* IMP1 Use constants for "sanitise" values. */ + + sorted_args = apr_array_make(msr->mp, 25, sizeof(const msc_arg *)); + + /* First we need to sort the arguments that need to be + * sanitised in descending order (we are using a stack structure + * to store then so the order will be ascending when we start + * popping them out). This is because we will + * be reading the request body sequentially and must + * sanitise it as we go. + */ + + for(;;) { + nextarg = NULL; + + /* Find the next largest offset (excluding + * the ones we've used up already). + */ + tarr = apr_table_elts(msr->arguments_to_sanitise); + telts = (const apr_table_entry_t*)tarr->elts; + for(i = 0; i < tarr->nelts; i++) { + msc_arg *arg = (msc_arg *)telts[i].val; + if (strcmp(arg->origin, "BODY") != 0) continue; + + if (last_offset == 0) { /* The first time we're here. */ + if (arg->value_origin_offset > offset) { + offset = arg->value_origin_offset; + nextarg = arg; + } + } else { /* Not the first time. */ + if ((arg->value_origin_offset > offset) + &&(arg->value_origin_offset < last_offset)) + { + offset = arg->value_origin_offset; + nextarg = arg; + } + } + } + + /* If we don't have the next argument that means + * we're done here. + */ + if (nextarg == NULL) break; + + sanitise = 2; /* Means time to pop the next argument out. */ + last_offset = offset; + offset = 0; + { /* IMP1 Fix this ugly bit here. */ + msc_arg **x = apr_array_push(sorted_args); + *x = nextarg; + } + } + + /* Now start retrieving the body chunk by chunk and + * sanitise data in pieces. + */ + + rc = modsecurity_request_body_retrieve_start(msr); + if (rc < 0) { + msr_log(msr, 1, "Audit log: Failed retrieving request body."); + } else { + msc_data_chunk *chunk = NULL; + unsigned int chunk_offset = 0; + unsigned int sanitise_offset = 0; + unsigned int sanitise_length = 0; + + text = apr_psprintf(msr->mp, "\n--%s-C--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + for(;;) { + rc = modsecurity_request_body_retrieve(msr, &chunk, -1); + if (chunk != NULL) { + /* Anything greater than 1 means we have more data to sanitise. */ + while (sanitise > 1) { + msc_arg **arg = NULL; + + if (sanitise == 2) { + /* Get the next argument from the stack. */ + arg = (msc_arg **)apr_array_pop(sorted_args); + if (arg == NULL) sanitise = 0; /* We're done sanitising. */ + else { + /* Continue with sanitation to process the + * retrieved argument. + */ + sanitise = 1; + sanitise_offset = (*arg)->value_origin_offset; + sanitise_length = (*arg)->value_origin_len; + } + } + + if (sanitise) { + /* Check if the data we want to sanitise is + * stored in the current chunk. + */ + if (chunk_offset + chunk->length > sanitise_offset) { + unsigned int soff; /* data offset within chunk */ + unsigned int len; /* amount in this chunk to sanitise */ + + soff = sanitise_offset - chunk_offset; + + if (soff + sanitise_length <= chunk->length) { + /* The entire argument resides in the current chunk. */ + len = sanitise_length; + sanitise = 2; /* Get another parameter to sanitise. */ + } else { + /* Some work to do here but we'll need to seek + * another chunk. + */ + len = chunk->length - soff; + sanitise_offset += len; + sanitise_length -= len; + sanitise = 1; /* It's OK to go to the next chunk. */ + } + + /* Yes, we actually write over the original data. + * We shouldn't be needing it any more. + */ + if (soff + len <= chunk->length) { /* double check */ + memset((char *)chunk->data + soff, '*', len); + } + } + } + } + + /* Write the sanitised chunk to the log + * and advance to the next chunk. */ + sec_auditlog_write(msr, chunk->data, chunk->length); + chunk_offset += chunk->length; + } + + if (rc <= 0) break; + } + + modsecurity_request_body_retrieve_end(msr); + } + } + } + + /* AUDITLOG_PART_FAKE_REQUEST_BODY */ + + if (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_FAKE_REQUEST_BODY) != NULL) { + if ((msr->msc_reqbody_read)&&(msr->mpd != NULL)) { + char *buffer = NULL; + + buffer = multipart_reconstruct_urlencoded_body_sanitise(msr); + if (buffer == NULL) { + msr_log(msr, 1, "Audit log: Failed to reconstruct request body."); + } else { + text = apr_psprintf(msr->mp, "\n--%s-I--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + sec_auditlog_write(msr, buffer, strlen(buffer)); + } + } + } + + /* AUDITLOG_PART_A_RESPONSE_HEADERS */ + + if (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_A_RESPONSE_HEADERS) != NULL) { + text = apr_psprintf(msr->mp, "\n--%s-F--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + /* There are no response headers (or the status line) in HTTP 0.9 */ + if (msr->response_headers_sent) { + if (msr->status_line != NULL) { + text = apr_psprintf(msr->mp, "%s %s\n", msr->response_protocol, + msr->status_line); + } else { + text = apr_psprintf(msr->mp, "%s %i\n", msr->response_protocol, + msr->response_status); + } + sec_auditlog_write(msr, text, strlen(text)); + + /* Output headers */ + + arr = apr_table_elts(msr->response_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + text = apr_psprintf(msr->mp, "%s: %s\n", te[i].key, te[i].val); + /* Do we need to sanitise this response header? */ + if (apr_table_get(msr->response_headers_to_sanitise, te[i].key) != NULL) { + /* Yes, sanitise it. */ + memset(text + strlen(te[i].key) + 2, '*', strlen(te[i].val)); + } + sec_auditlog_write(msr, text, strlen(text)); + } + } + } + + /* AUDITLOG_PART_RESPONSE_BODY */ + + if (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_RESPONSE_BODY) != NULL) { + if (msr->resbody_data != NULL) { + text = apr_psprintf(msr->mp, "\n--%s-E--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + sec_auditlog_write(msr, msr->resbody_data, msr->resbody_length); + wrote_response_body = 1; + } + } + + /* AUDITLOG_PART_TRAILER */ + + if (strchr(msr->txcfg->auditlog_parts, AUDITLOG_PART_TRAILER) != NULL) { + apr_time_t now = apr_time_now(); + + text = apr_psprintf(msr->mp, "\n--%s-H--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + /* Messages */ + for(i = 0; i < msr->alerts->nelts; i++) { + text = apr_psprintf(msr->mp, "Message: %s\n", ((char **)msr->alerts->elts)[i]); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Apache error messages */ + for(i = 0; i < msr->error_messages->nelts; i++) { + error_message *em = (((error_message**)msr->error_messages->elts)[i]); + text = apr_psprintf(msr->mp, "Apache-Error: %s\n", + format_error_log_message(msr->mp, em)); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Action */ + if (msr->was_intercepted) { + text = apr_psprintf(msr->mp, "Action: Intercepted (phase %i)\n", msr->intercept_phase); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Apache-Handler */ + if (msr->r->handler != NULL) { + text = apr_psprintf(msr->mp, "Apache-Handler: %s\n", msr->r->handler); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Processing times */ + if (msr->time_checkpoint_1 == 0) { + text = apr_psprintf(msr->mp, "Stopwatch: %" APR_TIME_T_FMT " %" APR_TIME_T_FMT + " (- - -)\n", (msr->request_time), (now - msr->request_time)); + } else { + char sw_str2[101] = "-"; + char sw_str3[101] = "-"; + + if (msr->time_checkpoint_2 != 0) { + apr_snprintf(sw_str2, sizeof(sw_str2), "%" APR_TIME_T_FMT, + (msr->time_checkpoint_2 - msr->request_time)); + } + + if (msr->time_checkpoint_3 != 0) { + apr_snprintf(sw_str3, sizeof(sw_str3), "%" APR_TIME_T_FMT, + (msr->time_checkpoint_3 - msr->request_time)); + } + + text = apr_psprintf(msr->mp, "Stopwatch: %" APR_TIME_T_FMT + " %" APR_TIME_T_FMT " (%" APR_TIME_T_FMT + "%s %s %s)\n", + (msr->request_time), (now - msr->request_time), + (msr->time_checkpoint_1 - msr->request_time), + ((msr->msc_reqbody_read == 0) ? "" : "*"), + sw_str2, sw_str3 + ); + } + + sec_auditlog_write(msr, text, strlen(text)); + + /* Our response body does not contain chunks */ + /* ENH Only write this when the output was chunked. */ + /* ENH Add info when request body was decompressed, dechunked too. */ + if (wrote_response_body) { + text = apr_psprintf(msr->mp, "Response-Body-Transformed: Dechunked\n"); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Producer */ + text = apr_psprintf(msr->mp, "Producer: %s\n", MODULE_NAME_FULL); + sec_auditlog_write(msr, text, strlen(text)); + + /* Server */ + if (msr->server_software != NULL) { + text = apr_psprintf(msr->mp, "Server: %s\n", msr->server_software); + sec_auditlog_write(msr, text, strlen(text)); + } + + /* Sanitised arguments */ + { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + + tarr = apr_table_elts(msr->arguments_to_sanitise); + telts = (const apr_table_entry_t*)tarr->elts; + + if (tarr->nelts > 0) { + text = apr_psprintf(msr->mp, "Sanitised-Args: "); + sec_auditlog_write(msr, text, strlen(text)); + } + + for(i = 0; i < tarr->nelts; i++) { + msc_arg *arg = (msc_arg *)telts[i].val; + text = apr_psprintf(msr->mp, "%s\"%s\"%s", ((i == 0) ? "" : ", "), + log_escape(msr->mp, arg->name), ((i == (tarr->nelts - 1)) ? ".\n" : "")); + sec_auditlog_write(msr, text, strlen(text)); + } + } + + /* Sanitised request headers */ + { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + + tarr = apr_table_elts(msr->request_headers_to_sanitise); + telts = (const apr_table_entry_t*)tarr->elts; + + if (tarr->nelts > 0) { + text = apr_psprintf(msr->mp, "Sanitised-Request-Headers: "); + sec_auditlog_write(msr, text, strlen(text)); + } + + for(i = 0; i < tarr->nelts; i++) { + text = apr_psprintf(msr->mp, "%s\"%s\"%s", ((i == 0) ? "" : ", "), + log_escape(msr->mp, telts[i].key), ((i == (tarr->nelts - 1)) ? ".\n" : "")); + sec_auditlog_write(msr, text, strlen(text)); + } + } + + /* Sanitised response headers */ + { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + + tarr = apr_table_elts(msr->response_headers_to_sanitise); + telts = (const apr_table_entry_t*)tarr->elts; + + if (tarr->nelts > 0) { + text = apr_psprintf(msr->mp, "Sanitised-Response-Headers: "); + sec_auditlog_write(msr, text, strlen(text)); + } + + for(i = 0; i < tarr->nelts; i++) { + text = apr_psprintf(msr->mp, "%s\"%s\"%s", ((i == 0) ? "" : ", "), + log_escape(msr->mp, telts[i].key), ((i == (tarr->nelts - 1)) ? ".\n" : "")); + sec_auditlog_write(msr, text, strlen(text)); + } + } + + /* Web application info. */ + if ( ((msr->txcfg->webappid != NULL)&&(strcmp(msr->txcfg->webappid, "default") != 0)) + || (msr->sessionid != NULL) || (msr->userid != NULL)) + { + text = apr_psprintf(msr->mp, "WebApp-Info: \"%s\" \"%s\" \"%s\"\n", + msr->txcfg->webappid == NULL ? "-" : log_escape(msr->mp, msr->txcfg->webappid), + msr->sessionid == NULL ? "-" : log_escape(msr->mp, msr->sessionid), + msr->userid == NULL ? "-" : log_escape(msr->mp, msr->userid)); + sec_auditlog_write(msr, text, strlen(text)); + } + } + + + /* AUDITLOG_PART_ENDMARKER */ + + text = apr_psprintf(msr->mp, "\n--%s-Z--\n", msr->new_auditlog_boundary); + sec_auditlog_write(msr, text, strlen(text)); + + /* Return here if we were writing to a serial log + * as it does not need an index file. + */ + if (msr->txcfg->auditlog_type != AUDITLOG_CONCURRENT) { + sec_auditlog_write(msr, "\n", 1); + + /* Unlock the mutex we used to serialise access to the audit log file. */ + rc = apr_global_mutex_unlock(msr->modsecurity->auditlog_lock); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Audit log: Failed to unlock global mutex: %s", + get_apr_error(msr->mp, rc)); + } + + return; + } + + /* From here on only concurrent-style processing. */ + + apr_file_close(msr->new_auditlog_fd); + + /* Write an entry to the index file */ + + /* Calculate hash of the entry. */ + apr_md5_final(md5hash, &msr->new_auditlog_md5ctx); + + str2 = apr_psprintf(msr->mp, "%s %i %i md5:%s", msr->new_auditlog_filename, 0, + msr->new_auditlog_size, bytes2hex(msr->mp, md5hash, 16)); + if (str2 == NULL) return; + + /* We do not want the index line to be longer than 3980 bytes. */ + limit = 3980; + was_limited = 0; + + /* If we are logging to a pipe we need to observe and + * obey the pipe atomic write limit - PIPE_BUF. For + * more details see the discussion in sec_guardian_logger code. + */ + if (msr->txcfg->auditlog_name[0] == '|') { + if (PIPE_BUF < limit) { + limit = PIPE_BUF; + } + } + + limit = limit - strlen(str2) - 5; + if (limit <= 0) { + msr_log(msr, 1, "Audit Log: Atomic PIPE write buffer too small: %i", PIPE_BUF); + return; + } + + str1 = construct_log_vcombinedus_limited(msr, limit, &was_limited); + if (str1 == NULL) return; + + if (was_limited == 0) { + text = apr_psprintf(msr->mp, "%s %s \n", str1, str2); + } else { + text = apr_psprintf(msr->mp, "%s %s L\n", str1, str2); + } + if (text == NULL) return; + + nbytes = strlen(text); + apr_file_write_full(msr->txcfg->auditlog_fd, text, nbytes, &nbytes_written); +} diff --git a/apache2/msc_logging.h b/apache2/msc_logging.h new file mode 100644 index 00000000..26ddeb0a --- /dev/null +++ b/apache2/msc_logging.h @@ -0,0 +1,46 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_logging.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_LOGGING_H_ +#define _MSC_LOGGING_H_ + +#define AUDITLOG_OFF 0 +#define AUDITLOG_ON 1 +#define AUDITLOG_RELEVANT 2 + +#define AUDITLOG_SERIAL 0 +#define AUDITLOG_CONCURRENT 1 + +#define AUDITLOG_PART_FIRST 'A' +#define AUDITLOG_PART_HEADER 'A' +#define AUDITLOG_PART_REQUEST_HEADERS 'B' +#define AUDITLOG_PART_REQUEST_BODY 'C' +#define AUDITLOG_PART_RESPONSE_HEADERS 'D' +#define AUDITLOG_PART_RESPONSE_BODY 'E' +#define AUDITLOG_PART_A_RESPONSE_HEADERS 'F' +#define AUDITLOG_PART_A_RESPONSE_BODY 'G' +#define AUDITLOG_PART_TRAILER 'H' +#define AUDITLOG_PART_FAKE_REQUEST_BODY 'I' +#define AUDITLOG_PART_LAST 'I' +#define AUDITLOG_PART_ENDMARKER 'Z' + +#include "modsecurity.h" + +int DSOLOCAL is_valid_parts_specification(char *p); + +char DSOLOCAL *construct_log_vcombinedus(modsec_rec *msr); + +char DSOLOCAL *construct_log_vcombinedus_limited(modsec_rec *msr, int _limit, int *was_limited); + +void DSOLOCAL sec_audit_logger(modsec_rec *msr); + +#endif diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c new file mode 100644 index 00000000..2b5b0946 --- /dev/null +++ b/apache2/msc_multipart.c @@ -0,0 +1,856 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_multipart.c,v 1.2 2006/10/16 04:41:51 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "msc_multipart.h" +#include "msc_util.h" + + +#if 0 +static char *multipart_construct_filename(modsec_rec *msr) { + char c, *p, *q = msr->mpd->mpp->filename; + char *filename; + + /* find the last backward slash and consider the + * filename to be only what's right from it + */ + p = strrchr(q, '\\'); + if (p != NULL) q = p + 1; + + /* do the same for the forward slash */ + p = strrchr(q, '/'); + if (p != NULL) q = p + 1; + + /* allow letters, digits and dots, replace + * everything else with underscores + */ + p = filename = apr_pstrdup(msr->mp, q); + while((c = *p) != 0) { + if (!( isalnum(c)||(c == '.') )) *p = '_'; + p++; + } + + return filename; +} +#endif + +/** + * + */ +static int multipart_parse_content_disposition(modsec_rec *msr, char *c_d_value) { + char *p = NULL, *t = NULL; + + /* accept only what we understand */ + if (strncmp(c_d_value, "form-data", 9) != 0) { + return -1; + } + + /* see if there are any other parts to parse */ + + p = c_d_value + 9; + while((*p == '\t')||(*p == ' ')) p++; + if (*p == '\0') return 1; /* this is OK */ + + if (*p != ';') return -2; + p++; + + /* parse the appended parts */ + + while(*p != '\0') { + char *name = NULL, *value = NULL, *start = NULL; + + /* go over the whitespace */ + while((*p == '\t')||(*p == ' ')) p++; + if (*p == '\0') return -3; + + start = p; + while((*p != '\0')&&(*p != '=')&&(*p != '\t')&&(*p != ' ')) p++; + if (*p == '\0') return -4; + + name = apr_pstrmemdup(msr->mp, start, (p - start)); + + while((*p == '\t')||(*p == ' ')) p++; + if (*p == '\0') return -5; + + if (*p != '=') return -13; + p++; + + while((*p == '\t')||(*p == ' ')) p++; + if (*p == '\0') return -6; + + if (*p == '"') { + /* quoted */ + + p++; + if (*p == '\0') return -7; + + start = p; + value = apr_pstrdup(msr->mp, p); + t = value; + + while(*p != '\0') { + if (*p == '\\') { + if (*(p + 1) == '\0') { + /* improper escaping */ + return -8; + } + /* only " and \ can be escaped */ + if ((*(p + 1) == '"')||(*(p + 1) == '\\')) { + p++; + } + else { + /* improper escaping */ + + /* We allow for now because IE sends + * improperly escaped content and there's + * nothing we can do about it. + * + * return -9; + */ + } + } + else + if (*p == '"') { + *t = '\0'; + break; + } + + *t++ = *p++; + } + if (*p == '\0') return -10; + + p++; /* go over the quote at the end */ + + } else { + /* not quoted */ + + start = p; + while((*p != '\0')&&(is_token_char(*p))) p++; + value = apr_pstrmemdup(msr->mp, start, (p - start)); + } + + /* evaluate part */ + + if (strcmp(name, "name") == 0) { + if (msr->mpd->mpp->name != NULL) return -14; + msr->mpd->mpp->name = value; + msr_log(msr, 9, "Multipart: Content-Disposition name: %s", + log_escape_nq(msr->mp, value)); + } + else + if (strcmp(name, "filename") == 0) { + if (msr->mpd->mpp->filename != NULL) return -15; + msr->mpd->mpp->filename = value; + msr_log(msr, 9, "Multipart: Content-Disposition filename: %s", + log_escape_nq(msr->mp, value)); + } + else return -11; + + if (*p != '\0') { + while((*p == '\t')||(*p == ' ')) p++; + /* the next character must be a zero or a semi-colon */ + if (*p == '\0') return 1; /* this is OK */ + if (*p != ';') return -12; + p++; /* move over the semi-colon */ + } + + /* loop will stop when (*p == '\0') */ + } + + return 1; +} + +/** + * + */ +static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { + int rc; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if ((msr->mpd->buf[0] == '\r') + &&(msr->mpd->buf[1] == '\n') + &&(msr->mpd->buf[2] == '\0')) + { + char *header_value; + + /* empty line */ + + header_value = (char *)apr_table_get(msr->mpd->mpp->headers, "Content-Disposition"); + if (header_value == NULL) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Part is missing the Content-Disposition header"); + return -1; + } + + rc = multipart_parse_content_disposition(msr, header_value); + if (rc < 0) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid Content-Disposition header (%i): %s", + rc, log_escape_nq(msr->mp, header_value)); + return -1; + } + + if (msr->mpd->mpp->name == NULL) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Part name missing"); + return -1; + } + + if (msr->mpd->mpp->filename != NULL) { + msr->mpd->mpp->type = MULTIPART_FILE; + } else { + msr->mpd->mpp->type = MULTIPART_FORMDATA; + } + + msr->mpd->mpp_state = 1; + msr->mpd->mpp->last_header_name = NULL; + } else { + /* header line */ + if ((msr->mpd->buf[0] == '\t')||(msr->mpd->buf[0] == ' ')) { + char *header_value, *new_value, *data; + + /* header folding, add data to the header we are building */ + + if (msr->mpd->mpp->last_header_name == NULL) { + /* we are not building a header at this moment */ + *error_msg = apr_psprintf(msr->mp, "Multipart: invalid part header (invalid folding)"); + return -1; + } + + /* locate the beginning of the data */ + data = msr->mpd->buf; + while((*data == '\t')||(*data == ' ')) data++; + + new_value = apr_pstrdup(msr->mp, data); + remove_lf_crlf_inplace(new_value); + + /* update the header value in the table */ + header_value = (char *)apr_table_get(msr->mpd->mpp->headers, msr->mpd->mpp->last_header_name); + new_value = apr_pstrcat(msr->mp, header_value, " ", new_value, NULL); + apr_table_set(msr->mpd->mpp->headers, msr->mpd->mpp->last_header_name, new_value); + + msr_log(msr, 9, "Multipart: Continued folder header \"%s\" with \"%s\"", + log_escape(msr->mp, msr->mpd->mpp->last_header_name), + log_escape(msr->mp, data)); + + if (strlen(new_value) > 4096) { + *error_msg = apr_psprintf(msr->mp, "Multpart: invalid part header (too long)"); + return -1; + } + } else { + char *header_name, *header_value, *data; + + /* new header */ + + data = msr->mpd->buf; + while((*data != ':')&&(*data != '\0')) data++; + if (*data == '\0') { + *error_msg = apr_psprintf(msr->mp, "Multipart: invalid part header (missing colon): %s", + log_escape_nq(msr->mp, msr->mpd->buf)); + return -1; + } + + header_name = apr_pstrmemdup(msr->mp, msr->mpd->buf, (data - msr->mpd->buf)); + + /* extract the value value */ + data++; + while((*data == '\t')||(*data == ' ')) data++; + header_value = apr_pstrdup(msr->mp, data); + remove_lf_crlf_inplace(header_value); + + /* error if the name already exists */ + if (apr_table_get(msr->mpd->mpp->headers, header_name) != NULL) { + *error_msg = apr_psprintf(msr->mp, "Multipart: part header already exists: %s", + log_escape_nq(msr->mp, header_name)); + return -1; + } + + apr_table_setn(msr->mpd->mpp->headers, header_name, header_value); + msr->mpd->mpp->last_header_name = header_name; + + msr_log(msr, 9, "Multipart: Added part header \"%s\" \"%s\"", + log_escape(msr->mp, header_name), + log_escape(msr->mp, header_value)); + } + } + + return 1; +} + +/** + * + */ +static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { + char *p = msr->mpd->buf + (MULTIPART_BUF_SIZE - msr->mpd->bufleft) - 2; + char localreserve[2]; + int bytes_reserved = 0; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* preserve the last two bytes for later */ + if (MULTIPART_BUF_SIZE - msr->mpd->bufleft >= 2) { + bytes_reserved = 1; + localreserve[0] = *p; + localreserve[1] = *(p + 1); + msr->mpd->bufleft += 2; + *p = 0; + } + + /* add data to the part we are building */ + if (msr->mpd->mpp->type == MULTIPART_FILE) { + + /* remember where we started */ + if (msr->mpd->mpp->length == 0) { + msr->mpd->mpp->offset = msr->mpd->buf_offset; + } + + /* only store individual files on disk if we are going + * to keep them or if we need to have them approved later + */ + if (msr->upload_extract_files) { + /* first create a temporary file if we don't have it already */ + if (msr->mpd->mpp->tmp_file_fd == 0) { + // char *filename = multipart_construct_filename(msr); + + /* construct temporary file name */ + msr->mpd->mpp->tmp_file_name = apr_psprintf(msr->mp, "%s/%s-%s-file-XXXXXX", + msr->txcfg->tmp_dir, current_filetime(msr->mp), msr->txid); + msr->mpd->mpp->tmp_file_fd = msc_mkstemp(msr->mpd->mpp->tmp_file_name); + + /* do we have an opened file? */ + if (msr->mpd->mpp->tmp_file_fd < 0) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Failed to create file: %s", + log_escape_nq(msr->mp, msr->mpd->mpp->tmp_file_name)); + return -1; + } + + msr_log(msr, 4, "Multipart: Created temporary file: %s", + log_escape_nq(msr->mp, msr->mpd->mpp->tmp_file_name)); + } + + /* write the reserve first */ + if (msr->mpd->reserve[0] == 1) { + if (write(msr->mpd->mpp->tmp_file_fd, &msr->mpd->reserve[1], 2) != 2) { + *error_msg = apr_psprintf(msr->mp, "Multipart: writing to \"%s\" failed", + log_escape(msr->mp, msr->mpd->mpp->tmp_file_name)); + return -1; + } + msr->mpd->mpp->tmp_file_size += 2; + msr->mpd->mpp->length += 2; + } + + /* write data to the file */ + if (write(msr->mpd->mpp->tmp_file_fd, msr->mpd->buf, MULTIPART_BUF_SIZE - msr->mpd->bufleft) + != (MULTIPART_BUF_SIZE - msr->mpd->bufleft)) + { + *error_msg = apr_psprintf(msr->mp, "Multipart: writing to \"%s\" failed", + log_escape(msr->mp, msr->mpd->mpp->tmp_file_name)); + return -1; + } + + msr->mpd->mpp->tmp_file_size += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + msr->mpd->mpp->length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + } else { + /* just keep track of the file size */ + if (msr->mpd->reserve[0] == 1) { + msr->mpd->mpp->tmp_file_size += 2; + } + msr->mpd->mpp->tmp_file_size += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + msr->mpd->mpp->length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + } + } + else if (msr->mpd->mpp->type == MULTIPART_FORMDATA) { + value_part_t *value_part = apr_pcalloc(msr->mp, sizeof(value_part_t)); + + /* add this part to the list of parts */ + + /* remember where we started */ + if (msr->mpd->mpp->length == 0) { + msr->mpd->mpp->offset = msr->mpd->buf_offset; + } + + if (msr->mpd->reserve[0] == 1) { + value_part->data = apr_palloc(msr->mp, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + 2); + memcpy(value_part->data, &(msr->mpd->reserve[1]), 2); + memcpy(value_part->data + 2, msr->mpd->buf, (MULTIPART_BUF_SIZE - msr->mpd->bufleft)); + value_part->length = (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + 2; + msr->mpd->mpp->length += value_part->length; + } else { + value_part->length = (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + value_part->data = apr_pstrmemdup(msr->mp, msr->mpd->buf, value_part->length); + msr->mpd->mpp->length += value_part->length; + } + + *(value_part_t **)apr_array_push(msr->mpd->mpp->value_parts) = value_part; + + msr_log(msr, 9, "Multipart: Added data to variable: %s", + log_escape_nq_ex(msr->mp, value_part->data, value_part->length)); + } + else { + *error_msg = apr_psprintf(msr->mp, "Multipart: unknown part type %i", msr->mpd->mpp->type); + return -1; + } + + /* store the reserved bytes to the multipart + * context so that they don't get lost + */ + if (bytes_reserved) { + msr->mpd->reserve[0] = 1; + msr->mpd->reserve[1] = localreserve[0]; + msr->mpd->reserve[2] = localreserve[1]; + msr->mpd->buf_offset += 2; + } + else { + msr->mpd->reserve[0] = 0; + msr->mpd->buf_offset -= 2; + } + + return 1; +} + +/** + * + */ +static char *multipart_combine_value_parts(modsec_rec *msr, apr_array_header_t *value_parts) { + value_part_t **parts = NULL; + char *rval = apr_palloc(msr->mp, msr->mpd->mpp->length + 1); + unsigned long int offset; + int i; + + if (rval == NULL) return NULL; + + offset = 0; + parts = (value_part_t **)value_parts->elts; + for(i = 0; i < value_parts->nelts; i++) { + if (offset + parts[i]->length <= msr->mpd->mpp->length) { + memcpy(rval + offset, parts[i]->data, parts[i]->length); + offset += parts[i]->length; + } + } + rval[offset] = '\0'; + + return rval; +} + +/** + * + */ +static int multipart_process_boundary(modsec_rec *msr, int last_part, char **error_log) { + /* if there was a part being built finish it */ + if (msr->mpd->mpp != NULL) { + /* close the temp file */ + if ((msr->mpd->mpp->type == MULTIPART_FILE) + &&(msr->mpd->mpp->tmp_file_name != NULL) + &&(msr->mpd->mpp->tmp_file_fd != 0)) + { + close(msr->mpd->mpp->tmp_file_fd); + } + + if (msr->mpd->mpp->type != MULTIPART_FILE) { + /* now construct a single string out of the parts */ + msr->mpd->mpp->value = multipart_combine_value_parts(msr, msr->mpd->mpp->value_parts); + if (msr->mpd->mpp->value == NULL) return -1; + } + + /* add the part to the list of parts */ + *(multipart_part **)apr_array_push(msr->mpd->parts) = msr->mpd->mpp; + if (msr->mpd->mpp->type == MULTIPART_FILE) { + msr_log(msr, 9, "Multipart: Added file part %x to the list: name \"%s\" " + "file name \"%s\" (offset %u, length %u)", + msr->mpd->mpp, log_escape(msr->mp, msr->mpd->mpp->name), + log_escape(msr->mp, msr->mpd->mpp->filename), + msr->mpd->mpp->offset, msr->mpd->mpp->length); + } + else { + msr_log(msr, 9, "Multipart: Added part %x to the list: name \"%s\" " + "(offset %u, length %u)", msr->mpd->mpp, log_escape(msr->mp, msr->mpd->mpp->name), + msr->mpd->mpp->offset, msr->mpd->mpp->length); + } + msr->mpd->mpp = NULL; + } + + if (last_part == 0) { + /* start building a new part */ + msr->mpd->mpp = (multipart_part *)apr_pcalloc(msr->mp, sizeof(multipart_part)); + if (msr->mpd->mpp == NULL) return -1; + msr->mpd->mpp->type = MULTIPART_FORMDATA; + msr->mpd->mpp_state = 0; + + msr->mpd->mpp->headers = apr_table_make(msr->mp, 10); + if (msr->mpd->mpp->headers == NULL) return -1; + msr->mpd->mpp->last_header_name = NULL; + + msr->mpd->reserve[0] = 0; + msr->mpd->reserve[1] = 0; + msr->mpd->reserve[2] = 0; + msr->mpd->reserve[3] = 0; + + msr->mpd->mpp->value_parts = apr_array_make(msr->mp, 10, sizeof(value_part_t *)); + } + + return 1; +} + + +/** + * + */ +int multipart_init(modsec_rec *msr, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + + msr->mpd = (multipart_data *)apr_pcalloc(msr->mp, sizeof(multipart_data)); + if (msr->mpd == NULL) return -1; + + if (msr->request_content_type == NULL) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Content-Type header not available."); + return -1; + } + + msr->mpd->boundary = strstr(msr->request_content_type, "boundary="); + if ((msr->mpd->boundary != NULL)&&(*(msr->mpd->boundary + 9) != 0)) { + msr->mpd->boundary = msr->mpd->boundary + 9; + } + else { + *error_msg = apr_psprintf(msr->mp, "Multipart Boundary not found or invalid."); + return -1; + } + + msr->mpd->parts = apr_array_make(msr->mp, 10, sizeof(multipart_part *)); + msr->mpd->bufleft = MULTIPART_BUF_SIZE; + msr->mpd->bufptr = msr->mpd->buf; + msr->mpd->buf_contains_line = 1; + msr->mpd->mpp = NULL; + + return 1; +} + +/** + * + */ +int multipart_complete(modsec_rec *msr, char **error_log) { + if (msr->mpd == NULL) return 1; + + if ((msr->mpd->seen_data != 0)&&(msr->mpd->is_complete == 0)) { + *error_log = apr_psprintf(msr->mp, "Multipart: final boundary missing"); + return -1; + } + + return 1; +} + +/** + * + */ +int multipart_process_chunk(modsec_rec *msr, const char *buf, + unsigned int size, char **error_msg) +{ + char *inptr = (char *)buf; + unsigned int inleft = size; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (size == 0) return 1; + + if (msr->mpd->seen_data == 0) msr->mpd->seen_data = 1; + + if (msr->mpd->is_complete) { + msr_log(msr, 4, "Multipart: Ignoring data after last boundary (received %i bytes)", size); + return 1; + } + + if (msr->mpd->bufleft == 0) { + *error_msg = apr_psprintf(msr->mp, + "Multipart: Internal error in process_chunk: no space left in the buffer"); + return -1; + } + + /* here we loop through the data available, byte by byte */ + while(inleft > 0) { + char c = *inptr; + int process_buffer = 0; + + if ((c == 0x0d)&&(msr->mpd->bufleft == 1)) { + /* we don't want to take 0x0d as the last byte in the buffer */ + process_buffer = 1; + } else { + inptr++; + inleft = inleft - 1; + + *(msr->mpd->bufptr) = c; + msr->mpd->bufptr++; + msr->mpd->bufleft--; + } + + /* until we either reach the end of the line + * or the end of our internal buffer + */ + if ((c == 0x0a)||(msr->mpd->bufleft == 0)||(process_buffer)) { + *(msr->mpd->bufptr) = 0; + + /* boundary preconditions: length of the line greater than + * the length of the boundary + the first two characters + * are dashes "-" + */ + if ( msr->mpd->buf_contains_line + && (strlen(msr->mpd->buf) > strlen(msr->mpd->boundary) + 2) + && (((*(msr->mpd->buf) == '-'))&&(*(msr->mpd->buf + 1) == '-')) + && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) ) { + + char *boundary_end = msr->mpd->buf + 2 + strlen(msr->mpd->boundary); + + if ( (*boundary_end == '\r') + &&(*(boundary_end + 1) == '\n') + &&(*(boundary_end + 2) == '\0') + ) { + /* simple boundary */ + if (multipart_process_boundary(msr, 0, error_msg) < 0) return -1; + } + else + if ( (*boundary_end == '-') + &&(*(boundary_end + 1) == '-') + &&(*(boundary_end + 2) == '\r') + &&(*(boundary_end + 3) == '\n') + &&(*(boundary_end + 4) == '\0') + ) { + /* final boundary */ + msr->mpd->is_complete = 1; + if (multipart_process_boundary(msr, 1, error_msg) < 0) return -1; + } + else { + /* error */ + *error_msg = apr_psprintf(msr->mp, + "Multipart: Invalid boundary detected: %s", + log_escape_nq(msr->mp, msr->mpd->buf)); + return -1; + } + } + else { + if (msr->mpd->mpp == NULL) { + msr_log(msr, 4, "Multipart: Ignoring data before first boundary."); + } else { + if (msr->mpd->mpp_state == 0) { + if ((msr->mpd->bufleft == 0)||(process_buffer)) { + /* part header lines must be shorter than + * MULTIPART_BUF_SIZE bytes + */ + *error_msg = apr_psprintf(msr->mp, + "Multipart: Part header line over %i bytes long", + MULTIPART_BUF_SIZE); + return -1; + } + if (multipart_process_part_header(msr, error_msg) < 0) return -1; + } else { + if (multipart_process_part_data(msr, error_msg) < 0) return -1; + } + } + } + + /* Update the offset of the data we are about + * to process. This is to allow us to know the + * offsets of individual files and variables. + */ + msr->mpd->buf_offset += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + + /* reset the pointer to the beginning of the buffer + * and continue to accept input data + */ + msr->mpd->bufptr = msr->mpd->buf; + msr->mpd->bufleft = MULTIPART_BUF_SIZE; + msr->mpd->buf_contains_line = (c == 0x0a) ? 1 : 0; + } + + if ((msr->mpd->is_complete)&&(inleft != 0)) { + msr_log(msr, 4, "Multipart: Ignoring data after last boundary (%i bytes left)", inleft); + return 1; + } + } + + return 1; +} + +/** + * + */ +apr_status_t multipart_cleanup(modsec_rec *msr) { + int keep_files = 0; + + if (msr->mpd == NULL) return -1; + + msr_log(msr, 4, "Multipart: Cleanup started (remove files %i).", msr->upload_remove_files); + + if (msr->upload_remove_files == 0) { + if (msr->txcfg->upload_dir == NULL) { + msr_log(msr, 1, "Input filter: SecUploadDir is undefined, unable to store " + "multipart files."); + } else { + keep_files = 1; + } + } + + /* Loop through the list of parts + * and delete the temporary files, but only if + * file storage was not requested, or if storage + * of relevant files was requested and this isn't + * such a request. + */ + if (keep_files == 0) { + multipart_part **parts; + int i; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FILE) { + if (parts[i]->tmp_file_name != NULL) { + if (unlink(parts[i]->tmp_file_name) < 0) { + msr_log(msr, 1, "Multipart: Failed to delete file (part) \"%s\" because %d(%s)", + log_escape(msr->mp, parts[i]->tmp_file_name), errno, strerror(errno)); + } else { + msr_log(msr, 4, "Multipart: Deleted file (part) \"%s\"", + log_escape(msr->mp, parts[i]->tmp_file_name)); + } + } + } + } + } else { + /* delete empty files, move the others to the upload dir */ + multipart_part **parts; + int i; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if ((parts[i]->type == MULTIPART_FILE)&&(parts[i]->tmp_file_size == 0)) { + /* Delete empty file. */ + if (parts[i]->tmp_file_name != NULL) { + if (unlink(parts[i]->tmp_file_name) < 0) { + msr_log(msr, 1, "Multipart: Failed to delete empty file (part) \"%s\" because %d(%s)", + log_escape(msr->mp, parts[i]->tmp_file_name), errno, strerror(errno)); + } else { + msr_log(msr, 4, "Multipart: Deleted empty file (part) \"%s\"", + log_escape(msr->mp, parts[i]->tmp_file_name)); + } + } + } else { + /* Move file to the upload dir. */ + if (parts[i]->tmp_file_name != NULL) { + const char *new_filename = NULL; + const char *new_basename = NULL; + + new_basename = file_basename(msr->mp, parts[i]->tmp_file_name); + if (new_basename == NULL) return -1; + new_filename = apr_psprintf(msr->mp, "%s/%s", msr->txcfg->upload_dir, + new_basename); + if (new_filename == NULL) return -1; + + if (apr_file_rename(parts[i]->tmp_file_name, new_filename, + msr->msc_reqbody_mp) != APR_SUCCESS) + { + msr_log(msr, 1, "Input filter: Failed to rename file from \"%s\" to \"%s\".", + log_escape(msr->mp, parts[i]->tmp_file_name), + log_escape(msr->mp, new_filename)); + return -1; + } else { + msr_log(msr, 4, "Input filter: Moved file from \"%s\" to \"%s\".", + log_escape(msr->mp, parts[i]->tmp_file_name), + log_escape(msr->mp, new_filename)); + } + } + } + } + } + + return 1; +} + +/** + * + */ +int multipart_get_arguments(modsec_rec *msr, char *origin, apr_table_t *arguments) { + multipart_part **parts; + int i; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FORMDATA) { + msc_arg *arg = (msc_arg *)apr_pcalloc(msr->mp, sizeof(msc_arg)); + if (arg == NULL) return -1; + + arg->name = parts[i]->name; + arg->name_len = strlen(parts[i]->name); // TODO + arg->value = parts[i]->value; + arg->value_len = parts[i]->length; + arg->value_origin_offset = parts[i]->offset; + arg->value_origin_len = parts[i]->length; + arg->origin = origin; + + apr_table_addn(arguments, arg->name, (void *)arg); + } + } + + return 1; +} + +/** + * + */ +char *multipart_reconstruct_urlencoded_body_sanitise(modsec_rec *msr) { + multipart_part **parts; + char *body; + unsigned int body_len; + int i; + + if (msr->mpd == NULL) return NULL; + + // TODO Cache this data somewhere in the structure + + /* calculate the size of the buffer */ + body_len = 1; + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FORMDATA) { + body_len += 4; + body_len += strlen(parts[i]->name) * 3; + body_len += strlen(parts[i]->value) * 3; + } + } + + /* allocate the buffer */ + body = apr_palloc(msr->mp, body_len + 1); + if ((body == NULL)||(body_len + 1 == 0)) return NULL; + *body = 0; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FORMDATA) { + if (*body != 0) { + strncat(body, "&", body_len - strlen(body)); + } + strnurlencat(body, parts[i]->name, body_len - strlen(body)); + strncat(body, "=", body_len - strlen(body)); + + /* Sanitise the variable. Since we are only doing this for + * the logging we will actually write over the data we keep + * in the memory. + */ + if (msr->phase >= PHASE_LOGGING) { + if (apr_table_get(msr->arguments_to_sanitise, parts[i]->name) != NULL) { + memset(parts[i]->value, '*', strlen(parts[i]->value)); + } + } + strnurlencat(body, parts[i]->value, body_len - strlen(body)); + } + } + + return body; +} diff --git a/apache2/msc_multipart.h b/apache2/msc_multipart.h new file mode 100644 index 00000000..81bf65d6 --- /dev/null +++ b/apache2/msc_multipart.h @@ -0,0 +1,120 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_multipart.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_MULTIPART_H_ +#define _MSC_MULTIPART_H_ + +#define MULTIPART_BUF_SIZE 4096 + +#define MULTIPART_FORMDATA 1 +#define MULTIPART_FILE 2 + +typedef struct multipart_part multipart_part; +typedef struct multipart_data multipart_data; + +#include "apr_general.h" +#include "apr_tables.h" +#include "modsecurity.h" + +typedef struct value_part_t value_part_t; +struct value_part_t { + char *data; + long int length; +}; + +struct multipart_part { + /* part type, can be MULTIPART_FORMDATA or MULTIPART_FILE */ + int type; + /* the name */ + char *name; + + /* variables only, variable value */ + char *value; + apr_array_header_t *value_parts; + + /* files only, the content type (where available) */ + char *content_type; + + /* files only, the name of the temporary file holding data */ + char *tmp_file_name; + int tmp_file_fd; + unsigned tmp_file_size; + /* files only, filename as supplied by the browser */ + char *filename; + + char *last_header_name; + apr_table_t *headers; + + unsigned int offset; + unsigned int length; +}; + +struct multipart_data { + /* this array keeps parts */ + apr_array_header_t *parts; + + /* mime boundary used to detect when + * parts end and new begin + */ + char *boundary; + + /* internal buffer and other variables + * used while parsing + */ + char buf[MULTIPART_BUF_SIZE + 2]; + int buf_contains_line; + char *bufptr; + int bufleft; + + unsigned int buf_offset; + + /* pointer that keeps track of a part while + * it is being built + */ + multipart_part *mpp; + + + /* part parsing state; 0 means we are reading + * headers, 1 means we are collecting data + */ + int mpp_state; + + /* because of the way this parsing algorithm + * works we hold back the last two bytes of + * each data chunk so that we can discard it + * later if the next data chunk proves to be + * a boundary; the first byte is an indicator + * 0 - no content, 1 - two data bytes available + */ + char reserve[4]; + + int seen_data; + int is_complete; +}; + + +/* Functions */ + +int DSOLOCAL multipart_init(modsec_rec *msr, char **error_msg); + +int DSOLOCAL multipart_complete(modsec_rec *msr, char **error_msg); + +int DSOLOCAL multipart_process_chunk(modsec_rec *msr, const char *buf, + unsigned int size, char **error_msg); + +apr_status_t DSOLOCAL multipart_cleanup(modsec_rec *msr); + +int DSOLOCAL multipart_get_arguments(modsec_rec *msr, char *origin, apr_table_t *arguments); + +char DSOLOCAL *multipart_reconstruct_urlencoded_body_sanitise(modsec_rec *msr); + +#endif diff --git a/apache2/msc_parsers.c b/apache2/msc_parsers.c new file mode 100644 index 00000000..3d2deff4 --- /dev/null +++ b/apache2/msc_parsers.c @@ -0,0 +1,305 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_parsers.c,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "msc_parsers.h" +#include + +/** + * + */ +int parse_cookies_v0(modsec_rec *msr, char *_cookie_header, apr_table_t *cookies) { + char *attr_name = NULL, *attr_value = NULL; + char *cookie_header; + char *saveptr = NULL; + int cookie_count = 0; + char *p = NULL; + + if (_cookie_header == NULL) { + msr_log(msr, 1, "Cookie parser: Received null for argument."); + return -1; + } + + cookie_header = strdup(_cookie_header); + if (cookie_header == NULL) return -1; + + p = apr_strtok(cookie_header, ";", &saveptr); + while(p != NULL) { + attr_name = NULL; + attr_value = NULL; + + /* ignore whitespace at the beginning of cookie name */ + while(isspace(*p)) p++; + attr_name = p; + + attr_value = strstr(p, "="); + if (attr_value != NULL) { + /* terminate cookie name */ + *attr_value = 0; + /* move over to the beginning of the value */ + attr_value++; + } + + /* we ignore cookies with empty names */ + if ((attr_name != NULL)&&(strlen(attr_name) != 0)) { + if (attr_value != NULL) { + msr_log(msr, 5, "Adding request cookie: name \"%s\", value \"%s\"", log_escape(msr->mp, attr_name), log_escape(msr->mp, attr_value)); + apr_table_add(cookies, attr_name, attr_value); + } else { + msr_log(msr, 5, "Adding request cookie: name \"%s\", value empty", log_escape(msr->mp, attr_name)); + apr_table_add(cookies, attr_name, ""); + } + + cookie_count++; + } + + p = apr_strtok(NULL, ";", &saveptr); + } + + free(cookie_header); + return cookie_count; +} + +/** + * + */ +int parse_cookies_v1(modsec_rec *msr, char *_cookie_header, apr_table_t *cookies) { + char *attr_name = NULL, *attr_value = NULL, *p = NULL; + char *prev_attr_name = NULL; + char *cookie_header = NULL; + int cookie_count = 0; + + if (_cookie_header == NULL) return -1; + cookie_header = strdup(_cookie_header); + if (cookie_header == NULL) return -1; + + p = cookie_header; + while(*p != 0) { + attr_name = NULL; + attr_value = NULL; + + /* attribute name */ + + /* remove space from the beginning */ + while((isspace(*p))&&(*p != 0)) p++; + attr_name = p; + while((*p != 0)&&(*p != '=')&&(*p != ';')&&(*p != ',')) p++; + + /* if we've reached the end of string */ + if (*p == 0) goto add_cookie; + + /* if there is no cookie value supplied */ + if ((*p == ';')||(*p == ',')) { + *p++ = 0; /* terminate the name */ + goto add_cookie; + } + + /* terminate the attribute name, + * writing over the = character + */ + *p++ = 0; + + /* attribute value */ + + /* skip over the whitespace at the beginning */ + while((isspace(*p))&&(*p != 0)) p++; + + /* no value supplied */ + if (*p == 0) goto add_cookie; + + if (*p == '"') { + if (*++p == 0) goto add_cookie; + attr_value = p; + while((*p != 0)&&(*p != '"')) p++; + if (*p != 0) *p++ = 0; + else { + /* Do nothing about this. */ + } + } else { + attr_value = p; + while((*p != 0)&&(*p != ',')&&(*p != ';')) p++; + if (*p != 0) *p++ = 0; + + /* remove the whitespace from the end of cookie value */ + if (attr_value != NULL) { + char *t = attr_value; + int i = 0; + + while(*t != 0) { + t++; + i++; + } + + while((i-- > 0)&&(isspace(*(--t)))) *t = 0; + } + } + + add_cookie: + + /* remove the whitespace from the end of cookie name */ + if (attr_name != NULL) { + char *t = attr_name; + int i = 0; + + while(*t != 0) { + t++; + i++; + } + + while((i-- > 0)&&(isspace(*(--t)))) *t = 0; + } + + /* add the cookie to the list now */ + if ((attr_name != NULL)&&(strlen(attr_name) != 0)) { + + /* handle special attribute names */ + if (attr_name[0] == '$') { + if (prev_attr_name != NULL) { + /* cookie keyword, we change the name we use + * so they can have a unique name in the cookie table + */ + attr_name = apr_psprintf(msr->mp, "$%s_%s", prev_attr_name, attr_name + 1); + } + } + + if (attr_value != NULL) { + msr_log(msr, 5, "Adding request cookie: name \"%s\", value \"%s\"", + log_escape(msr->mp, attr_name), log_escape(msr->mp, attr_value)); + apr_table_add(cookies, attr_name, attr_value); + } else { + msr_log(msr, 5, "Adding request cookie: name \"%s\", value empty", + log_escape(msr->mp, attr_name)); + apr_table_add(cookies, attr_name, ""); + } + + cookie_count++; + + /* only keep the cookie names for later */ + if (attr_name[0] != '$') prev_attr_name = attr_name; + } + + /* at this point the *p is either 0 (in which case we exit), or + * right after the current cookie ended - we need to look for + * the next cookie + */ + while( (*p != 0)&&( (*p == ',')||(*p == ';')||(isspace(*p)) ) ) p++; + } + + return cookie_count; +} + +/** + * + */ +int parse_arguments(modsec_rec *msr, const char *s, int argument_separator, const char *origin, + apr_table_t *arguments, int *invalid_count) +{ + msc_arg *arg; + long inputlength, i, j; + char *value = NULL; + char *buf; + int status; + + if (s == NULL) return -1; + inputlength = strlen(s); + if (inputlength == 0) return 1; + if (inputlength + 1 <= 0) return -1; + + buf = (char *)malloc(inputlength + 1); + if (buf == NULL) return -1; + + arg = (msc_arg *)apr_pcalloc(msr->mp, sizeof(msc_arg)); + arg->origin = origin; + + i = 0; + j = 0; + status = 0; + *invalid_count = 0; + while (i < inputlength) { + if (status == 0) { + /* parameter name */ + arg->name_origin_offset = i; + while ((s[i] != '=') && (s[i] != argument_separator) && (i < inputlength)) { + buf[j] = s[i]; + j++; + i++; + } + buf[j++] = '\0'; + arg->name_origin_len = i - arg->name_origin_offset; + } else { + /* parameter value */ + arg->value_origin_offset = i; + while ((s[i] != argument_separator) && (i < inputlength)) { + buf[j] = s[i]; + j++; + i++; + } + buf[j++] = '\0'; + arg->value_origin_len = i - arg->value_origin_offset; + } + + if (status == 0) { + arg->name_len = urldecode_nonstrict_inplace_ex(buf, arg->name_origin_len, invalid_count); + arg->name = apr_pstrmemdup(msr->mp, buf, arg->name_len); + + if (s[i] == argument_separator) { + /* Empty parameter */ + arg->value_len = 0; + arg->value = ""; + + apr_table_addn(arguments, arg->name, (void *)arg); + msr_log(msr, 5, "Adding request argument (%s): name \"%s\", value \"%s\"", + arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), + log_escape_ex(msr->mp, arg->value, arg->value_len)); + + arg = (msc_arg *)apr_pcalloc(msr->mp, sizeof(msc_arg)); + arg->origin = origin; + + status = 0; /* unchanged */ + j = 0; + } else { + status = 1; + value = &buf[j]; + } + } + else { + arg->value_len = urldecode_nonstrict_inplace_ex(value, arg->value_origin_len, invalid_count); + arg->value = apr_pstrmemdup(msr->mp, value, arg->value_len); + + apr_table_addn(arguments, arg->name, (void *)arg); + msr_log(msr, 5, "Adding request argument (%s): name \"%s\", value \"%s\"", + arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), + log_escape_ex(msr->mp, arg->value, arg->value_len)); + + arg = (msc_arg *)apr_pcalloc(msr->mp, sizeof(msc_arg)); + arg->origin = origin; + + status = 0; + j = 0; + } + + i++; /* skip over the separator */ + } + + /* the last parameter was empty */ + if (status == 1) { + arg->value_len = 0; + arg->value = ""; + + apr_table_addn(arguments, arg->name, (void *)arg); + msr_log(msr, 5, "Adding request argument (%s): name \"%s\", value \"%s\"", + arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), + log_escape_ex(msr->mp, arg->value, arg->value_len)); + } + + free(buf); + return 1; +} diff --git a/apache2/msc_parsers.h b/apache2/msc_parsers.h new file mode 100644 index 00000000..89a49875 --- /dev/null +++ b/apache2/msc_parsers.h @@ -0,0 +1,25 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_parsers.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_PARSERS_H_ +#define _MSC_PARSERS_H_ + +#include "modsecurity.h" + +int DSOLOCAL parse_cookies_v0(modsec_rec *msr, char *_cookie_header, apr_table_t *cookies); + +int DSOLOCAL parse_cookies_v1(modsec_rec *msr, char *_cookie_header, apr_table_t *cookies); + +int DSOLOCAL parse_arguments(modsec_rec *msr, const char *s, int argument_separator, + const char *origin, apr_table_t *arguments, int *invalid_count); + +#endif diff --git a/apache2/msc_pcre.c b/apache2/msc_pcre.c new file mode 100644 index 00000000..4729d3b9 --- /dev/null +++ b/apache2/msc_pcre.c @@ -0,0 +1,93 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_pcre.c,v 1.2 2006/12/28 10:39:13 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "msc_pcre.h" +#include "apr_strings.h" + +/** + * Releases the resources used by a single regular expression pattern. + */ +apr_status_t msc_pcre_cleanup(msc_regex_t *regex) { + if (regex != NULL) { + if (regex->pe != NULL) { + free(regex->pe); + regex->pe = NULL; + } + if (regex->re != NULL) { + free(regex->re); + regex->re = NULL; + } + } + + return APR_SUCCESS; +} + +/** + * Compiles the provided regular expression pattern. The last two + * parameters are optional, but if they are provided and an error + * occurs they will contain the error message and the offset in + * the pattern where the offending part of the pattern begins. + */ +void *msc_pregcomp(apr_pool_t *pool, const char *pattern, int options, + const char **_errptr, int *_erroffset) +{ + const char *errptr = NULL; + int erroffset; + msc_regex_t *regex; + + regex = apr_pcalloc(pool, sizeof(msc_regex_t)); + if (regex == NULL) return NULL; + regex->pattern = pattern; + + if ((_errptr == NULL)||(_erroffset == NULL)) { + regex->re = pcre_compile(pattern, options, &errptr, &erroffset, NULL); + } else { + regex->re = pcre_compile(pattern, options, _errptr, _erroffset, NULL); + } + if (regex->re == NULL) return NULL; + + #ifdef WITH_PCRE_STUDY + regex->pe = pcre_study(regex->re, 0, &errptr); + #endif + + apr_pool_cleanup_register(pool, (void *)regex, + (apr_status_t (*)(void *))msc_pcre_cleanup, apr_pool_cleanup_null); + + return regex; +} + +/** + * Executes regular expression, capturing subexpressions in the given + * vector. Returns PCRE_ERROR_NOMATCH when there is no match, error code < -1 + * on errors, and a value > 0 when there is a match. + */ +int msc_regexec_capture(msc_regex_t *regex, const char *s, unsigned int slen, + int *ovector, int ovecsize, char **error_msg) +{ + if (error_msg == NULL) return -1000; /* To differentiate from PCRE as it already uses -1. */ + *error_msg = NULL; + + return pcre_exec(regex->re, regex->pe, s, slen, 0, 0, ovector, ovecsize); +} + +/** + * Executes regular expression but ignores any of the subexpression + * captures. See above for the return codes. + */ +int msc_regexec(msc_regex_t *regex, const char *s, unsigned int slen, + char **error_msg) +{ + if (error_msg == NULL) return -1000; /* To differentiate from PCRE as it already uses -1. */ + *error_msg = NULL; + + return msc_regexec_capture(regex, s, slen, NULL, 0, error_msg); +} diff --git a/apache2/msc_pcre.h b/apache2/msc_pcre.h new file mode 100644 index 00000000..8cc9dc33 --- /dev/null +++ b/apache2/msc_pcre.h @@ -0,0 +1,39 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_pcre.h,v 1.3 2006/12/28 10:39:13 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_PCRE_H_ +#define _MSC_PCRE_H_ + +typedef struct msc_regex_t msc_regex_t; + +#include "pcre.h" +#include "apr_general.h" +#include "modsecurity.h" + +struct msc_regex_t { + void *re; + void *pe; + const char *pattern; +}; + +apr_status_t DSOLOCAL msc_pcre_cleanup(msc_regex_t *regex); + +void DSOLOCAL *msc_pregcomp(apr_pool_t *pool, const char *pattern, int options, + const char **_errptr, int *_erroffset); + +int DSOLOCAL msc_regexec_capture(msc_regex_t *regex, const char *s, + unsigned int slen, int *ovector, int ovecsize, char **error_msg); + +int DSOLOCAL msc_regexec(msc_regex_t *regex, const char *s, unsigned int slen, + char **error_msg); + +#endif diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c new file mode 100644 index 00000000..f60893e5 --- /dev/null +++ b/apache2/msc_reqbody.c @@ -0,0 +1,636 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_reqbody.c,v 1.2 2006/12/04 21:54:10 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "modsecurity.h" +#include "msc_parsers.h" + +#define CHUNK_CAPACITY 8192 + +/** + * Prepare to accept the request body (part 2). + */ +static apr_status_t modsecurity_request_body_start_init(modsec_rec *msr) { + if(msr->msc_reqbody_storage == MSC_REQBODY_MEMORY) { + /* Prepare to store request body in memory. */ + + msr->msc_reqbody_chunks = apr_array_make(msr->msc_reqbody_mp, + 32, sizeof(msc_data_chunk *)); + if (msr->msc_reqbody_chunks == NULL) return -1; + } else { + /* Prepare to store request body on disk. */ + + msr->msc_reqbody_filename = apr_psprintf(msr->mp, "%s/%s-%s-request_body-XXXXXX", + msr->txcfg->tmp_dir, current_filetime(msr->mp), msr->txid); + if (msr->msc_reqbody_filename == NULL) return -1; + + msr->msc_reqbody_fd = msc_mkstemp((char *)msr->msc_reqbody_filename); + if (msr->msc_reqbody_fd < 0) { + msr_log(msr, 1, "Input filter: Failed to create temporary file: %s", + msr->msc_reqbody_filename); + return -1; + } + + msr_log(msr, 4, "Input filter: Created temporary file to store request body: %s", + msr->msc_reqbody_filename); + } + + return 1; +} + +/** + * Prepare to accept the request body (part 1). + */ +apr_status_t modsecurity_request_body_start(modsec_rec *msr) { + msr->msc_reqbody_length = 0; + + /* Create a separate memory pool that will be used + * to allocate structures from (not data, which is allocated + * via malloc). + */ + apr_pool_create(&msr->msc_reqbody_mp, msr->mp); + + /* Initialise request body processors, if any. */ + + if (msr->msc_reqbody_processor != NULL) { + char *my_error_msg = NULL; + + if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { + if (multipart_init(msr, &my_error_msg) < 0) { + msr_log(msr, 1, "Multipart parser init failed: %s", my_error_msg); + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + } + } + #ifdef WITH_LIBXML2 + else + if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { + if (xml_init(msr, &my_error_msg) < 0) { + msr_log(msr, 1, "XML parser init failed: %s", my_error_msg); + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + } + } + #endif + else + if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { + /* Do nothing, URLENCODED processor does not support streaming yet. */ + } + else { + msr_log(msr, 1, "Unknown request body processor: %s", msr->msc_reqbody_processor); + return -1; + } + } + + return modsecurity_request_body_start_init(msr); +} + +/** + * Stores a chunk of request body data to disk. + */ +static apr_status_t modsecurity_request_body_store_disk(modsec_rec *msr, + const char *data, apr_size_t length) +{ + apr_size_t i = write(msr->msc_reqbody_fd, data, length); + if (i != length) { + msr_log(msr, 1, "Input filter: Failed writing %lu bytes to temporary file (rc %lu).", i); + return -1; + } + + return 1; +} + +/** + * Stores one chunk of request body data in memory. + */ +static apr_status_t modsecurity_request_body_store_memory(modsec_rec *msr, + const char *data, apr_size_t length) +{ + /* Would storing this chunk mean going over the limit? */ + if ((msr->msc_reqbody_spilltodisk) + && (msr->msc_reqbody_length + length > (apr_size_t)msr->txcfg->reqbody_inmemory_limit)) + { + msc_data_chunk **chunks; + unsigned int disklen = 0; + int i; + + msr_log(msr, 4, "Input filter: Request too large to store in memory, switching to disk."); + + /* NOTE Must use modsecurity_request_body_store_disk() here + * to prevent data to be sent to the streaming + * processors again. + */ + + /* Initialise disk storage */ + msr->msc_reqbody_storage = MSC_REQBODY_DISK; + if (modsecurity_request_body_start_init(msr) < 0) return -1; + + /* Write the data we keep in memory */ + chunks = (msc_data_chunk **)msr->msc_reqbody_chunks->elts; + for(i = 0; i < msr->msc_reqbody_chunks->nelts; i++) { + disklen += chunks[i]->length; + + if (modsecurity_request_body_store_disk(msr, chunks[i]->data, chunks[i]->length) < 0) { + return -1; + } + + free(chunks[i]->data); + chunks[i]->data = NULL; + } + + /* Clear the memory pool as we no longer need the bits. */ + + /* IMP1 But since we only used apr_pool_clear memory might + * not be released back to the OS straight away? + */ + msr->msc_reqbody_chunks = NULL; + apr_pool_clear(msr->msc_reqbody_mp); + + msr_log(msr, 4, "Input filter: Wrote %lu bytes from memory to disk.", disklen); + + /* Continue with disk storage from now on */ + return modsecurity_request_body_store_disk(msr, data, length); + } + + /* If we're here that means we are not over the + * request body in-memory limit yet. + */ + { + unsigned long int bucket_offset, bucket_left; + + bucket_offset = 0; + bucket_left = length; + + /* Although we store the request body in chunks we don't + * want to use the same chunk sizes as the incoming memory + * buffers. They are often of very small sizes and that + * would make us waste a lot of memory. That's why we + * use our own chunks of CHUNK_CAPACITY sizes. + */ + + /* Loop until we empty this bucket into our chunks. */ + while(bucket_left > 0) { + /* Allocate a new chunk if we have to. */ + if (msr->msc_reqbody_chunk_current == NULL) { + msr->msc_reqbody_chunk_current = (msc_data_chunk *) + apr_pcalloc(msr->msc_reqbody_mp, sizeof(msc_data_chunk)); + if (msr->msc_reqbody_chunk_current == NULL) return -1; + + msr->msc_reqbody_chunk_current->data = malloc(CHUNK_CAPACITY); + if (msr->msc_reqbody_chunk_current->data == NULL) return -1; + + msr->msc_reqbody_chunk_current->length = 0; + msr->msc_reqbody_chunk_current->is_permanent = 1; + + *(const msc_data_chunk **)apr_array_push(msr->msc_reqbody_chunks) + = msr->msc_reqbody_chunk_current; + } + + if (bucket_left < (CHUNK_CAPACITY - msr->msc_reqbody_chunk_current->length)) { + /* There's enough space in the current chunk. */ + memcpy(msr->msc_reqbody_chunk_current->data + + msr->msc_reqbody_chunk_current->length, data + bucket_offset, bucket_left); + msr->msc_reqbody_chunk_current->length += bucket_left; + bucket_left = 0; + } else { + /* Fill the existing chunk. */ + unsigned long int copy_length = CHUNK_CAPACITY - + msr->msc_reqbody_chunk_current->length; + + memcpy(msr->msc_reqbody_chunk_current->data + + msr->msc_reqbody_chunk_current->length, data + bucket_offset, copy_length); + bucket_offset += copy_length; + bucket_left -= copy_length; + msr->msc_reqbody_chunk_current->length += copy_length; + + /* We're done with this chunk. Setting the pointer + * to NULL is going to force a new chunk to be allocated + * on the next go. + */ + msr->msc_reqbody_chunk_current = NULL; + } + } + + msr->msc_reqbody_length += length; + } + + return 1; +} + +/** + * Stores one chunk of request body data. Returns -1 on error. + */ +apr_status_t modsecurity_request_body_store(modsec_rec *msr, + const char *data, apr_size_t length) +{ + /* If we have a processor for this request body send + * data to it first (but only if it did not report an + * error on previous invocations). + */ + if ((msr->msc_reqbody_processor != NULL)&&(msr->msc_reqbody_error == 0)) { + char *my_error_msg = NULL; + + if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { + if (multipart_process_chunk(msr, data, length, &my_error_msg) < 0) { + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + msr_log(msr, 4, "%s", my_error_msg); + } + } + #ifdef WITH_LIBXML2 + else + if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { + if (xml_process_chunk(msr, data, length, &my_error_msg) < 0) { + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + msr_log(msr, 4, "%s", my_error_msg); + } + } + #endif + else + if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { + /* Do nothing, URLENCODED processor does not support streaming. */ + } + else { + msr_log(msr, 1, "Unknown request body processor: %s", msr->msc_reqbody_processor); + return -1; + } + } + + /* Store data. */ + if (msr->msc_reqbody_storage == MSC_REQBODY_MEMORY) { + return modsecurity_request_body_store_memory(msr, data, length); + } + else + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + return modsecurity_request_body_store_disk(msr, data, length); + } + + /* Should never happen. */ + msr_log(msr, 1, "Internal Error: Unknown value for msc_reqbody_storage: %i", + msr->msc_reqbody_storage); + return -1; +} + +/** + * + */ +static apr_status_t modsecurity_request_body_end_urlencoded(modsec_rec *msr) { + msc_data_chunk **chunks, *one_chunk; + char *d; + int i, sofar; + int invalid_count = 0; + + /* Allocate a buffer large enough to hold the request body. */ + + if (msr->msc_reqbody_length + 1 == 0) return -1; + msr->msc_reqbody_buffer = malloc(msr->msc_reqbody_length + 1); + if (msr->msc_reqbody_buffer == NULL) { + msr_log(msr, 1, "Unable to allocate memory to hold request body. Asked for %lu bytes.", + msr->msc_reqbody_length + 1); + return -1; + } + msr->msc_reqbody_buffer[msr->msc_reqbody_length] = '\0'; + + /* Copy the data we keep in chunks into the new buffer. */ + + sofar = 0; + d = msr->msc_reqbody_buffer; + chunks = (msc_data_chunk **)msr->msc_reqbody_chunks->elts; + for(i = 0; i < msr->msc_reqbody_chunks->nelts; i++) { + if (sofar + chunks[i]->length <= msr->msc_reqbody_length) { + memcpy(d, chunks[i]->data, chunks[i]->length); + d += chunks[i]->length; + sofar += chunks[i]->length; + } else { + msr_log(msr, 1, "Internal error, request body buffer overflow."); + return -1; + } + } + + /* Now free the memory used by the chunks. */ + + chunks = (msc_data_chunk **)msr->msc_reqbody_chunks->elts; + for(i = 0; i < msr->msc_reqbody_chunks->nelts; i++) { + free(chunks[i]->data); + chunks[i]->data = NULL; + } + + /* Create a new array with only one chunk in it. */ + + msr->msc_reqbody_chunks = apr_array_make(msr->msc_reqbody_mp, 2, sizeof(msc_data_chunk *)); + if (msr->msc_reqbody_chunks == NULL) return -1; + one_chunk = (msc_data_chunk *)apr_pcalloc(msr->msc_reqbody_mp, sizeof(msc_data_chunk)); + one_chunk->data = msr->msc_reqbody_buffer; + one_chunk->length = msr->msc_reqbody_length; + one_chunk->is_permanent = 1; + *(const msc_data_chunk **)apr_array_push(msr->msc_reqbody_chunks) = one_chunk; + + /* Parse URL-encoded arguments in the request body. */ + + if (parse_arguments(msr, msr->msc_reqbody_buffer, msr->txcfg->argument_separator, + "BODY", msr->arguments, &invalid_count) < 0) + { + msr_log(msr, 1, "Initialisation: Error occurred while parsing BODY arguments."); + return -1; + } + + return 1; +} + +/** + * Stops receiving the request body. + */ +apr_status_t modsecurity_request_body_end(modsec_rec *msr) { + + /* Close open file descriptors, if any. */ + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + if (msr->msc_reqbody_fd > 0) { + close(msr->msc_reqbody_fd); + msr->msc_reqbody_fd = -1; + } + } + + msr->msc_reqbody_read = 1; + + if ((msr->msc_reqbody_processor != NULL)&&(msr->msc_reqbody_error == 0)) { + char *my_error_msg = NULL; + + if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { + if (multipart_complete(msr, &my_error_msg) < 0) { + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + msr_log(msr, 1, "Multipart error: %s", my_error_msg); + return -1; + } + + if (multipart_get_arguments(msr, "BODY", msr->arguments) < 0) { + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = "Error retrieving arguments."; + msr_log(msr, 1, "Multipart error: %s", my_error_msg); + return -1; + } + } + else + if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { + return modsecurity_request_body_end_urlencoded(msr); + } + #ifdef WITH_LIBXML2 + else + if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { + if (xml_complete(msr, &my_error_msg) < 0) { + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; + msr_log(msr, 4, "%s", my_error_msg); + return -1; + } + } + #endif + } + + return 1; +} + +/** + * Prepares to forward the request body. + */ +apr_status_t modsecurity_request_body_retrieve_start(modsec_rec *msr) { + if (msr->msc_reqbody_storage == MSC_REQBODY_MEMORY) { + msr->msc_reqbody_chunk_position = 0; + msr->msc_reqbody_chunk_offset = 0; + + msr->msc_reqbody_disk_chunk = apr_pcalloc(msr->msc_reqbody_mp, sizeof(msc_data_chunk)); + if (msr->msc_reqbody_disk_chunk == NULL) return -1; + msr->msc_reqbody_disk_chunk->is_permanent = 1; + } + else + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + msr->msc_reqbody_disk_chunk = apr_pcalloc(msr->msc_reqbody_mp, sizeof(msc_data_chunk)); + if (msr->msc_reqbody_disk_chunk == NULL) return -1; + + msr->msc_reqbody_disk_chunk->is_permanent = 0; + msr->msc_reqbody_disk_chunk->data = apr_palloc(msr->msc_reqbody_mp, CHUNK_CAPACITY); + if (msr->msc_reqbody_disk_chunk->data == NULL) return -1; + + msr->msc_reqbody_fd = open(msr->msc_reqbody_filename, O_RDONLY | O_BINARY); + if (msr->msc_reqbody_fd < 0) { + msr_log(msr, 1, "Input filter: Failed to open temporary file for reading: %s", + msr->msc_reqbody_filename); + return -1; + } + } + + return 1; +} + +/** + * + */ +apr_status_t modsecurity_request_body_retrieve_end(modsec_rec *msr) { + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + if (msr->msc_reqbody_fd > 0) { + close(msr->msc_reqbody_fd); + msr->msc_reqbody_fd = -1; + } + } + + return 1; +} + +/** + * Returns one chunk of request body data. It stores a NULL + * in the chunk pointer when there is no data to return. The + * return code is 1 if more calls can be made to retrieve more + * data, 0 if there is no more data to retrieve, or -1 on error. + * + * The caller can limit the amount of data returned by providing + * a non-negative value in nbytes. + */ +apr_status_t modsecurity_request_body_retrieve(modsec_rec *msr, + msc_data_chunk **chunk, long int nbytes) +{ + msc_data_chunk **chunks; + + if (chunk == NULL) return -1; + *chunk = NULL; + + if (msr->msc_reqbody_storage == MSC_REQBODY_MEMORY) { + /* Are there any chunks left? */ + if (msr->msc_reqbody_chunk_position >= msr->msc_reqbody_chunks->nelts) { + /* No more chunks. */ + return 0; + } + + /* We always respond with the same chunk, just different information in it. */ + *chunk = msr->msc_reqbody_disk_chunk; + + /* Advance to the current chunk and position on the + * next byte we need to send. + */ + chunks = (msc_data_chunk **)msr->msc_reqbody_chunks->elts; + msr->msc_reqbody_disk_chunk->data = chunks[msr->msc_reqbody_chunk_position]->data + + msr->msc_reqbody_chunk_offset; + + if (nbytes < 0) { + /* Send what's left in this chunk as there is no limit on the size. */ + msr->msc_reqbody_disk_chunk->length = chunks[msr->msc_reqbody_chunk_position]->length; + msr->msc_reqbody_chunk_position++; + msr->msc_reqbody_chunk_offset = 0; + } else { + /* We have a limit we must obey. */ + + if (chunks[msr->msc_reqbody_chunk_position]->length - + msr->msc_reqbody_chunk_offset <= (unsigned int)nbytes) + { + /* If what's left in our chunk is less than the limit + * then send it all back. + */ + msr->msc_reqbody_disk_chunk->length = + chunks[msr->msc_reqbody_chunk_position]->length - + msr->msc_reqbody_chunk_offset; + msr->msc_reqbody_chunk_position++; + msr->msc_reqbody_chunk_offset = 0; + } else { + /* If we have more data in our chunk, send the + * maximum bytes we can (nbytes). + */ + msr->msc_reqbody_disk_chunk->length = nbytes; + msr->msc_reqbody_chunk_offset += nbytes; + } + } + + /* If we've advanced beyond our last chunk then + * we have no more data to send. + */ + if (msr->msc_reqbody_chunk_position >= msr->msc_reqbody_chunks->nelts) { + return 0; /* No more chunks. */ + } + + /* More data available. */ + return 1; + } + + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + long int my_nbytes = CHUNK_CAPACITY; + int i; + + /* Send CHUNK_CAPACITY bytes at a time unless a lower limit was requested. */ + if ((nbytes != -1)&&(my_nbytes > nbytes)) { + my_nbytes = nbytes; + } + + i = read(msr->msc_reqbody_fd, msr->msc_reqbody_disk_chunk->data, my_nbytes); + if (i < 0) { + msr_log(msr, 1, "Input filter: Error reading from temporary file: %s", + strerror(errno)); + return -1; + } + + *chunk = msr->msc_reqbody_disk_chunk; + msr->msc_reqbody_disk_chunk->length = i; + + if (i == 0) return 0; /* No more data available. */ + + return 1; /* More data available. */ + } + + msr_log(msr, 1, "Internal error, invalid msc_reqbody_storage value: %i", + msr->msc_reqbody_storage); + + return -1; +} + +/** + * + */ +apr_status_t modsecurity_request_body_clear(modsec_rec *msr) { + /* Release memory we used to store request body data. */ + if (msr->msc_reqbody_chunks != NULL) { + msc_data_chunk **chunks = (msc_data_chunk **)msr->msc_reqbody_chunks->elts; + int i; + + for(i = 0; i < msr->msc_reqbody_chunks->nelts; i++) { + if (chunks[i]->data != NULL) { + free(chunks[i]->data); + chunks[i]->data = NULL; + } + } + } + + if (msr->msc_reqbody_storage == MSC_REQBODY_DISK) { + int keep_body = 0; + + /* Should we keep the body? This normally + * happens when a PUT method was used, which + * means the body is actually a file. + */ + if ((msr->upload_remove_files == 0)&&(strcasecmp(msr->request_method, "PUT") == 0)) { + if (msr->txcfg->upload_dir != NULL) { + keep_body = 1; + } else { + msr_log(msr, 1, "Input filter: SecUploadDir is undefined, " + "unable to store PUT file."); + } + } + + /* Deal with a request body stored in a file. */ + + if (msr->msc_reqbody_filename != NULL) { + if (keep_body) { + /* Move request body (which is a file) to the storage area. */ + const char *put_filename = NULL; + const char *put_basename = NULL; + + /* Construct the new filename. */ + put_basename = file_basename(msr->msc_reqbody_mp, msr->msc_reqbody_filename); + if (put_basename == NULL) return -1; + put_filename = apr_psprintf(msr->msc_reqbody_mp, "%s/%s", + msr->txcfg->upload_dir, put_basename); + if (put_filename == NULL) return -1; + + if (apr_file_rename(msr->msc_reqbody_filename, put_filename, + msr->msc_reqbody_mp) != APR_SUCCESS) + { + msr_log(msr, 1, "Failed to rename file from \"%s\" to \"%s\".", + log_escape(msr->msc_reqbody_mp, msr->msc_reqbody_filename), + log_escape(msr->msc_reqbody_mp, put_filename)); + return -1; + } else { + msr_log(msr, 4, "Moved file from \"%s\" to \"%s\".", + log_escape(msr->msc_reqbody_mp, msr->msc_reqbody_filename), + log_escape(msr->msc_reqbody_mp, put_filename)); + } + } else { + /* We do not want to keep the request body. */ + if (apr_file_remove(msr->msc_reqbody_filename, + msr->msc_reqbody_mp) != APR_SUCCESS) + { + msr_log(msr, 1, "Failed to delete temporary file: %s", + msr->msc_reqbody_filename); + return -1; + } + + msr_log(msr, 4, "Input filter: Removed temporary file: %s", + msr->msc_reqbody_filename); + } + + msr->msc_reqbody_filename = NULL; + } + } + + /* NOTE No need to clear the pool as it has already been destroyed + * if (msr->msc_reqbody_mp != NULL) { + * apr_pool_clear(msr->msc_reqbody_mp); + * } + */ + + return 1; +} diff --git a/apache2/msc_util.c b/apache2/msc_util.c new file mode 100644 index 00000000..261745f8 --- /dev/null +++ b/apache2/msc_util.c @@ -0,0 +1,981 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_util.c,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "msc_util.h" + +#include +#include +#include +#include +#include + +/** + * + */ +int parse_boolean(const char *input) { + if (input == NULL) return -1; + if (strcasecmp(input, "on") == 0) return 1; + if (strcasecmp(input, "true") == 0) return 1; + if (strcasecmp(input, "1") == 0) return 1; + if (strcasecmp(input, "off") == 0) return 0; + if (strcasecmp(input, "false") == 0) return 0; + if (strcasecmp(input, "0") == 0) return 0; + + return -1; +} + +/** + * Parses a string that contains a name-value pair in the form "name=value". + * IMP1 It does not check for whitespace between tokens. + */ +int parse_name_eq_value(apr_pool_t *mp, const char *input, char **name, char **value) { + char *p = NULL; + + if ((name == NULL)||(value == NULL)) return -1; + if (input == NULL) return 0; + + *name = NULL; + *value = NULL; + p = (char *)input; + + while((*p != '=')&&(*p != '\0')) p++; + if (*p == '\0') { + *name = (char *)input; + return 1; + } + + *name = apr_pstrmemdup(mp, input, p - input); + if (*name == NULL) return -1; + p++; + + *value = apr_pstrdup(mp, p); + if (*value == NULL) return -1; + + return 1; +} + +/** + * + */ +char *url_encode(apr_pool_t *mp, char *input, unsigned int input_len) { + char *rval, *d; + unsigned int i, len; + + len = input_len * 3 + 1; + d = rval = apr_palloc(mp, len); + if (rval == NULL) return NULL; + + /* ENH Only encode the characters that really need to be encoded. */ + + for(i = 0; i < input_len; i++) { + unsigned char c = input[i]; + + if (c == ' ') { + *d++ = '+'; + } else + if ( (c == 42) || ((c >= 48)&&(c <= 57)) || ((c >= 65)&&(c <= 90)) + || ((c >= 97)&&(c <= 122)) + ) { + *d++ = c; + } else { + *d++ = '%'; + c2x(c, (unsigned char *)d); + d += 2; + } + } + + *d = '\0'; + + return rval; +} + +/** + * Appends an URL-encoded version of the source string to the + * destination string, but makes sure that no more than "maxlen" + * bytes are added. + */ +char *strnurlencat(char *destination, char *source, unsigned int maxlen) { + char *s = source; + char *d = destination; + + /* ENH Only encode the characters that really need to be encoded. */ + + /* Advance to the end of destination string. */ + while(*d != '\0') d++; + + /* Loop while there's bytes in the source string or + * until we reach the output limit. + */ + while((*s != '\0')&&(maxlen > 0)) { + unsigned char c = *s; + + if (c == ' ') { + *d++ = '+'; + maxlen--; + } else + if ( (c == 42) || ((c >= 48)&&(c <= 57)) || ((c >= 65)&&(c <= 90)) + || ((c >= 97)&&(c <= 122)) + ) { + *d++ = c; + maxlen--; + } else { + if (maxlen >= 3) { + *d++ = '%'; + c2x(c, (unsigned char *)d); + d += 2; + maxlen -= 3; + } else { + /* If there's not enough room for the encoded + * byte we ignore it. + */ + maxlen = 0; + } + } + + s++; + } + + *d++ = '\0'; + + return destination; +} + +/** + * + */ +char *file_basename(apr_pool_t *mp, const char *filename) { + char *d, *p; + + if (filename == NULL) return NULL; + d = apr_pstrdup(mp, filename); + if (d == NULL) return NULL; + + p = strrchr(d, '/'); + if (p != NULL) d = p + 1; + p = strrchr(d, '\\'); + if (p != NULL) d = p + 1; + + return d; +} + +/** + * + */ +#ifdef WIN32 +char *file_dirname(apr_pool_t *p, const char *filename) { + char *b, *c, *d; + + if (filename == NULL) return NULL; + b = apr_pstrdup(p, filename); + if (b == NULL) return NULL; + + c = strrchr(b, '/'); + if (c != NULL) { + d = strrchr(c, '\\'); + if (d != NULL) *d = '\0'; + else *c = '\0'; + } else { + d = strrchr(b, '\\'); + if (d != NULL) *d = '\0'; + } + + return b; +} +#else +char *file_dirname(apr_pool_t *p, const char *filename) { + char *b, *c; + + if (filename == NULL) return NULL; + b = apr_pstrdup(p, filename); + if (b == NULL) return NULL; + + c = strrchr(b, '/'); + if (c != NULL) *c = '\0'; + + return b; +} +#endif + + +/** + * + */ +int hex2bytes_inplace(unsigned char *data, int len) { + unsigned char *d = data; + int i, count = 0; + + if ((data == NULL)||(len == 0)) return 0; + + for(i = 0; i <= len - 2; i += 2) { + *d++ = x2c(&data[i]); + count++; + } + *d = '\0'; + + return count; +} + +/** + * Converts a series of bytes into its hexadecimal + * representation. + */ +char *bytes2hex(apr_pool_t *pool, unsigned char *data, int len) { + static unsigned char b2hex[] = "0123456789abcdef"; + char *hex = NULL; + int i, j; + + hex = apr_palloc(pool, (len * 2) + 1); + if (hex == NULL) return NULL; + + j = 0; + for(i = 0; i < len; i++) { + hex[j++] = b2hex[data[i] >> 4]; + hex[j++] = b2hex[data[i] & 0x0f]; + } + hex[j] = 0; + + return hex; +} + +/** + * + */ +int is_token_char(unsigned char c) { + /* ENH Is the performance important at all? We could use a table instead. */ + + /* CTLs not allowed */ + if ((c <= 32)||(c >= 127)) return 0; + + switch(c) { + case '(' : + case ')' : + case '<' : + case '>' : + case '@' : + case ',' : + case ';' : + case ':' : + case '\\' : + case '"' : + case '/' : + case '[' : + case ']' : + case '?' : + case '=' : + return 0; + } + + return 1; +} + +/** + * + */ +int remove_lf_crlf_inplace(char *text) { + char *p = text; + int count = 0; + + if (text == NULL) return -1; + + while(*p != '\0') { + count++; + p++; + } + + if (count > 0) { + if (*(p - 1) == '\n') { + *(p - 1) = '\0'; + if (count > 1) { + if (*(p - 2) == '\r') { + *(p - 2) = '\0'; + } + } + } + } + + return 1; +} + +/** + * Converts a byte given as its hexadecimal representation + * into a proper byte. Handles uppercase and lowercase letters + * but does not check for overflows. + */ +unsigned char x2c(unsigned char *what) { + register unsigned char digit; + + digit = (what[0] >= 'A' ? ((what[0] & 0xdf) - 'A') + 10 : (what[0] - '0')); + digit *= 16; + digit += (what[1] >= 'A' ? ((what[1] & 0xdf) - 'A') + 10 : (what[1] - '0')); + + return digit; +} + +/** + * + */ +char *guess_tmp_dir(apr_pool_t *p) { + char *filename = NULL; + + /* ENH Use apr_temp_dir_get instead. */ + + #ifdef WIN32 + filename = apr_pcalloc(p, 256); + if (filename == NULL) return ""; + if (GetTempPath(255, filename) != 0) return filename; + #endif + + filename = getenv("TMPDIR"); + if (filename != NULL) return filename; + + filename = getenv("TEMP"); + if (filename != NULL) return filename; + + filename = getenv("TMP"); + if (filename != NULL) return filename; + + #if defined NETWARE + return("sys:/tmp/"); + #elif defined WIN32 + return(""); + #else + return("/tmp/"); + #endif +} + +/** + * + */ +char *current_logtime(apr_pool_t *mp) { + apr_time_exp_t t; + char tstr[100]; + apr_size_t len; + + apr_time_exp_lt(&t, apr_time_now()); + + apr_strftime(tstr, &len, 80, "%d/%b/%Y:%H:%M:%S ", &t); + apr_snprintf(tstr + strlen(tstr), 80 - strlen(tstr), "%c%.2d%.2d", + t.tm_gmtoff < 0 ? '-' : '+', + t.tm_gmtoff / (60 * 60), t.tm_gmtoff % (60 * 60)); + return apr_pstrdup(mp, tstr); +} + +/** + * + */ +char *current_filetime(apr_pool_t *mp) { + apr_time_exp_t t; + char tstr[100]; + apr_size_t len; + + apr_time_exp_lt(&t, apr_time_now()); + + apr_strftime(tstr, &len, 80, "%Y%m%d-%H%M%S", &t); + return apr_pstrdup(mp, tstr); +} + +/** + * + */ +int msc_mkstemp(char *template) { + /* ENH Use apr_file_mktemp instead. */ + + #if !(defined(WIN32)||defined(NETWARE)) + return mkstemp(template); + #else + if (mktemp(template) == NULL) return -1; + return open(template, O_WRONLY | O_APPEND | O_CREAT | O_BINARY, CREATEMODE_UNISTD); + #endif +} + +/** + * Converts the input string to lowercase (in-place). + */ +char *strtolower_inplace(unsigned char *str) { + unsigned char *c = str; + + if (str == NULL) return NULL; + + while(*c != 0) { + *c = tolower(*c); + c++; + } + + return str; +} + +/** + * Converts a single byte into its hexadecimal representation. + * Will overwrite two bytes at the destination. + */ +unsigned char *c2x(unsigned what, unsigned char *where) { + static const char c2x_table[] = "0123456789abcdef"; + + what = what & 0xff; + *where++ = c2x_table[what >> 4]; + *where++ = c2x_table[what & 0x0f]; + + return where; +} + +char *log_escape(apr_pool_t *mp, const char *text) { + return _log_escape(mp, text, strlen(text), 1, 0); +} + +char *log_escape_nq(apr_pool_t *mp, const char *text) { + return _log_escape(mp, text, strlen(text), 0, 0); +} + +char *log_escape_ex(apr_pool_t *mp, const char *text, unsigned long int text_length) { + return _log_escape(mp, text, text_length, 1, 0); +} + +char *log_escape_nq_ex(apr_pool_t *mp, const char *text, unsigned long int text_length) { + return _log_escape(mp, text, text_length, 0, 0); +} + +char *log_escape_header_name(apr_pool_t *mp, const char *text) { + return _log_escape(mp, text, strlen(text), 0, 1); +} + +/** + * Transform input into a form safe for logging. + */ +char *_log_escape(apr_pool_t *mp, const unsigned char *input, unsigned long int input_len, + int escape_quotes, int escape_colon) +{ + unsigned char *d = NULL; + char *ret = NULL; + unsigned long int i; + + if (input == NULL) return NULL; + + ret = apr_palloc(mp, input_len * 4 + 1); + if (ret == NULL) return NULL; + d = (unsigned char *)ret; + + i = 0; + while(i < input_len) { + switch(input[i]) { + case ':' : + if (escape_colon) { + *d++ = '\\'; + *d++ = ':'; + } else { + *d++ = input[i]; + } + break; + case '"' : + if (escape_quotes) { + *d++ = '\\'; + *d++ = '"'; + } else { + *d++ = input[i]; + } + break; + case '\b' : + *d++ = '\\'; + *d++ = 'b'; + break; + case '\n' : + *d++ = '\\'; + *d++ = 'n'; + break; + case '\r' : + *d++ = '\\'; + *d++ = 'r'; + break; + case '\t' : + *d++ = '\\'; + *d++ = 't'; + break; + case '\v' : + *d++ = '\\'; + *d++ = 'v'; + break; + case '\\' : + *d++ = '\\'; + *d++ = '\\'; + break; + default : + if ((input[i] <= 0x1f)||(input[i] >= 0x7f)) { + *d++ = '\\'; + *d++ = 'x'; + c2x(input[i], d); + d += 2; + } else { + *d++ = input[i]; + } + break; + } + + i++; + } + + *d = 0; + + return ret; +} + +#define VALID_HEX(X) (((X >= '0')&&(X <= '9')) || ((X >= 'a')&&(X <= 'f')) || ((X >= 'A')&&(X <= 'F'))) + +/** + * + */ +int urldecode_uni_nonstrict_inplace_ex(char *input, long int input_len) { + unsigned char *d = (unsigned char *)input; + long int i, count; + + if (input == NULL) return -1; + + i = count = 0; + while (i < input_len) { + if (input[i] == '%') { + /* Character is a percent sign. */ + + if ((i + 1 < input_len)&&( (input[i + 1] == 'u')||(input[i + 1] == 'U') )) { + /* IIS-specific %u encoding. */ + if (i + 5 < input_len) { + /* We have at least 4 data bytes. */ + if ( (VALID_HEX(input[i + 2]))&&(VALID_HEX(input[i + 3])) + &&(VALID_HEX(input[i + 4]))&&(VALID_HEX(input[i + 5])) ) + { + /* We make use of the lower byte here, ignoring the higher byte. */ + *d++ = x2c(&input[i + 4]); + count++; + i += 6; + } else { + /* Invalid data. */ + int j; + + for(j = 0; (j < 6)&&(i < input_len); j++) { + *d++ = input[i++]; + count++; + } + } + } else { + /* Not enough bytes available (4 data bytes were needed). */ + while(i < input_len) { + *d++ = input[i++]; + count++; + } + } + } + else { + /* Standard URL encoding. */ + + /* Are there enough bytes available? */ + if (i + 2 < input_len) { + /* Yes. */ + + /* Decode a %xx combo only if it is valid. + */ + char c1 = input[i + 1]; + char c2 = input[i + 2]; + + /* ENH Use VALID_HEX? */ + if ( (((c1 >= '0')&&(c1 <= '9')) || ((c1 >= 'a')&&(c1 <= 'f')) || + ((c1 >= 'A')&&(c1 <= 'F'))) + && (((c2 >= '0')&&(c2 <= '9')) || ((c2 >= 'a')&&(c2 <= 'f')) || + ((c2 >= 'A')&&(c2 <= 'F'))) ) + { + *d++ = x2c(&input[i + 1]); + count++; + i += 3; + } else { + /* Not a valid encoding, copy the raw input bytes. */ + *d++ = '%'; + *d++ = c1; + *d++ = c2; + count += 3; + i += 3; + } + } else { + /* Not enough bytes available. */ + + *d++ = '%'; + count++; + i++; + + if (i + 1 < input_len) { + *d++ = input[i]; + count++; + i++; + } + } + } + } + else { + /* Character is not a percent sign. */ + if (input[i] == '+') { + *d++ = ' '; + } else { + *d++ = input[i]; + } + + count++; + i++; + } + } + + *d = '\0'; + + return count; +} + +/** + * + */ +int urldecode_nonstrict_inplace_ex(char *input, long int input_len, int *invalid_count) { + unsigned char *d = (unsigned char *)input; + long int i, count; + + if (input == NULL) return -1; + + i = count = 0; + while (i < input_len) { + if (input[i] == '%') { + /* Character is a percent sign. */ + + /* Are there enough bytes available? */ + if (i + 2 < input_len) { + char c1 = input[i + 1]; + char c2 = input[i + 2]; + + /* ENH Use VALID_HEX? */ + if ( (((c1 >= '0')&&(c1 <= '9')) || ((c1 >= 'a')&&(c1 <= 'f')) || ((c1 >= 'A')&&(c1 <= 'F'))) + && (((c2 >= '0')&&(c2 <= '9')) || ((c2 >= 'a')&&(c2 <= 'f')) || ((c2 >= 'A')&&(c2 <= 'F'))) ) + { + /* Valid encoding - decode it. */ + *d++ = x2c(&input[i + 1]); + count++; + i += 3; + } else { + /* Invalid encoding, just copy the raw bytes. */ + *d++ = '%'; + *d++ = c1; + *d++ = c2; + count += 3; + i += 3; + *invalid_count++; + } + } else { + /* Not enough bytes available, copy the raw bytes. */ + *invalid_count++; + + *d++ = '%'; + count++; + i++; + + if (i + 1 < input_len) { + *d++ = input[i]; + count++; + i++; + } + } + } else { + /* Character is not a percent sign. */ + if (input[i] == '+') { + *d++ = ' '; + } else { + *d++ = input[i]; + } + count++; + i++; + } + } + + *d = '\0'; + + return count; +} + +/** + * + */ +int html_entities_decode_inplace(apr_pool_t *mp, unsigned char *input, int input_len) { + unsigned char *d = input; + int i, count; + + if ((input == NULL)||(input_len <= 0)) return 0; + + i = count = 0; + while((i < input_len)&&(count < input_len)) { + int z, copy = 1; + + /* Require an ampersand and at least one character to + * start looking into the entity. + */ + if ((input[i] == '&')&&(i + 1 < input_len)) { + int k, j = i + 1; + + if (input[j] == '#') { + /* Numerical entity. */ + copy++; + + if (!(j + 1 < input_len)) goto HTML_ENT_OUT; /* Not enough bytes. */ + j++; + + if ((input[j] == 'x')||(input[j] == 'X')) { + /* Hexadecimal entity. */ + copy++; + + if (!(j + 1 < input_len)) goto HTML_ENT_OUT; /* Not enough bytes. */ + j++; /* j is the position of the first digit now. */ + + k = j; + while((j < input_len)&&(isxdigit(input[j]))) j++; + if (j > k) { /* Do we have at least one digit? */ + /* Decode the entity. */ + char *x = apr_pstrmemdup(mp, &input[k], j - k); + *d++ = (unsigned char)strtol(x, NULL, 16); + count++; + + /* Skip over the semicolon if it's there. */ + if ((j < input_len)&&(input[j] == ';')) i = j + 1; + else i = j; + + continue; + } else { + goto HTML_ENT_OUT; + } + } else { + /* Decimal entity. */ + k = j; + while((j < input_len)&&(isdigit(input[j]))) j++; + if (j > k) { /* Do we have at least one digit? */ + /* Decode the entity. */ + char *x = apr_pstrmemdup(mp, &input[k], j - k); + *d++ = (unsigned char)strtol(x, NULL, 10); + count++; + + /* Skip over the semicolon if it's there. */ + if ((j < input_len)&&(input[j] == ';')) i = j + 1; + else i = j; + + continue; + } else { + goto HTML_ENT_OUT; + } + } + } else { + /* Text entity. */ + + k = j; + while((j < input_len)&&(isalnum(input[j]))) j++; + if (j > k) { /* Do we have at least one digit? */ + char *x = apr_pstrmemdup(mp, &input[k], j - k); + + /* Decode the entity. */ + if (strcasecmp(x, "quot") == 0) *d++ = '"'; + else + if (strcasecmp(x, "amp") == 0) *d++ = '&'; + else + if (strcasecmp(x, "lt") == 0) *d++ = '<'; + else + if (strcasecmp(x, "gt") == 0) *d++ = '>'; + else + if (strcasecmp(x, "nbsp") == 0) *d++ = NBSP; + else { + /* We do no want to convert this entity, copy the raw data over. */ + copy = j - k + 1; + goto HTML_ENT_OUT; + } + + count++; + + /* Skip over the semicolon if it's there. */ + if ((j < input_len)&&(input[j] == ';')) i = j + 1; + else i = j; + + continue; + } + } + } + + HTML_ENT_OUT: + + for(z = 0; ((z < copy) && (count < input_len)); z++) { + *d++ = input[i++]; + count++; + } + } + + *d = '\0'; + + return count; +} + +#define ISODIGIT(X) ((X >= '0')&&(X <= '7')) + +int ansi_c_sequences_decode_inplace(unsigned char *input, int input_len) { + unsigned char *d = input; + int i, count; + + i = count = 0; + while(i < input_len) { + if ((input[i] == '\\')&&(i + 1 < input_len)) { + int c = -1; + + /* ENH Should we handle \c as well? + * See http://www.opengroup.org/onlinepubs/009695399/utilities/printf.html + */ + + switch(input[i + 1]) { + case 'a' : + c = '\a'; + break; + case 'b' : + c = '\b'; + break; + case 'f' : + c = '\f'; + break; + case 'n' : + c = '\n'; + break; + case 'r' : + c = '\r'; + break; + case 't' : + c = '\t'; + break; + case 'v' : + c = '\v'; + break; + case '\\' : + c = '\\'; + break; + case '?' : + c = '?'; + break; + case '\'' : + c = '\''; + break; + case '"' : + c = '"'; + break; + } + + if (c != -1) i += 2; + + /* Hexadecimal or octal? */ + if (c == -1) { + if ((input[i + 1] == 'x')||(input[i + 1] == 'X')) { + /* Hexadecimal. */ + if ((i + 3 < input_len)&&(isxdigit(input[i + 2]))&&(isxdigit(input[i + 3]))) { + /* Two digits. */ + c = x2c(&input[i + 2]); + i += 4; + } else { + /* Invalid encoding, do nothing. */ + } + } + else + if (isdigit(input[i + 1])) { /* Octal. */ + char buf[10]; + int j = 0, l = 3; + + /* Up to 4 digits if the first digit is a zero. */ + if (input[i + 1] == '0') l = 4; + + while((i + 1 + j < input_len)&&(j <= l)) { + buf[j] = input[i + 1 + j]; + j++; + if (!ISODIGIT(input[i + 1 + j])) break; + } + buf[j] = '\0'; + + if (j > 0) { + c = strtol(buf, NULL, 8); + i += 1 + j; + } + } + } + + if (c == -1) { + /* Didn't recognise encoding, copy raw bytes. */ + *d++ = input[i + 1]; + count++; + i += 2; + } else { + /* Converted the encoding. */ + *d++ = c; + count++; + } + } else { + /* Input character not a backslash, copy it. */ + *d++ = input[i++]; + count++; + } + } + + *d = '\0'; + + return count; +} + +int normalise_path_inplace(unsigned char *input, int input_len, int win) { + unsigned char *d = input; + int i, count; + + i = count = 0; + while ((i < input_len)&&(count < input_len)) { + char c = input[i]; + + /* Convert backslash to forward slash on Windows only. */ + if ((win)&&(c == '\\')) c = '/'; + + if (c == '/') { + /* Is there a directory back-reference? Yes, we + * require at least 5 prior bytes here. That's on + * purpose. + */ + if ((count >= 5)&&(*(d - 1) == '.')&&(*(d - 2) == '.')&&(*(d - 3) == '/')) { + char *cd = d - 4; + int ccount = count - 4; + + /* Go back until we reach the beginning or a forward slash. */ + while ((ccount > 0)&&(*cd != '/')) { + ccount--; + cd--; + } + + if (*cd == '/') { + d = cd; + count = ccount; + } + } else + /* Is there a directory self-reference? */ + if ((count >= 2)&&(*(d - 1) == '.')&&(*(d - 2) == '/')) { + /* Ignore the last two bytes. */ + d -= 2; + count -= 2; + } else + /* Or are there just multiple occurences of forward slash? */ + if ((count >= 1)&&(*(d - 1) == '/')) { + /* Ignore the last one byte. */ + d--; + count--; + } + } + + /* Copy the byte over. */ + *d++ = c; + count++; + i++; + } + + *d = '\0'; + + return count; +} diff --git a/apache2/msc_util.h b/apache2/msc_util.h new file mode 100644 index 00000000..a3e2ae76 --- /dev/null +++ b/apache2/msc_util.h @@ -0,0 +1,75 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_util.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _UTIL_H_ +#define _UTIL_H_ + +#include "modsecurity.h" + +int DSOLOCAL normalise_path_inplace(unsigned char *input, int len, int win); + +int DSOLOCAL parse_boolean(const char *input); + +int DSOLOCAL parse_name_eq_value(apr_pool_t *mp, const char *input, char **name, char **value); + +char DSOLOCAL *url_encode(apr_pool_t *mp, char *input, unsigned int input_len); + +char DSOLOCAL *strnurlencat(char *destination, char *source, unsigned int maxlen); + +char DSOLOCAL *file_dirname(apr_pool_t *p, const char *filename); + +char DSOLOCAL *file_basename(apr_pool_t *p, const char *filename); + +int DSOLOCAL hex2bytes_inplace(unsigned char *data, int len); + +char DSOLOCAL *bytes2hex(apr_pool_t *pool, unsigned char *data, int len); + +int DSOLOCAL is_token_char(unsigned char c); + +int DSOLOCAL remove_lf_crlf_inplace(char *text); + +unsigned DSOLOCAL char x2c(unsigned char *what); + +char DSOLOCAL *guess_tmp_dir(apr_pool_t *p); + +char DSOLOCAL *current_logtime(apr_pool_t *mp); + +char DSOLOCAL *current_filetime(apr_pool_t *mp); + +int DSOLOCAL msc_mkstemp(char *template); + +char DSOLOCAL *strtolower_inplace(unsigned char *str); + +unsigned char DSOLOCAL *c2x(unsigned what, unsigned char *where); + +char DSOLOCAL *log_escape(apr_pool_t *p, const char *text); + +char DSOLOCAL *log_escape_nq(apr_pool_t *p, const char *text); + +char DSOLOCAL *log_escape_ex(apr_pool_t *p, const char *text, unsigned long int text_length); + +char DSOLOCAL *log_escape_nq_ex(apr_pool_t *p, const char *text, unsigned long int text_length); + +char DSOLOCAL *log_escape_header_name(apr_pool_t *p, const char *text); + +char DSOLOCAL *_log_escape(apr_pool_t *p, const unsigned char *input, + unsigned long int input_length, int escape_quotes, int escape_colon); + +int DSOLOCAL urldecode_uni_nonstrict_inplace_ex(char *input, long int input_length); + +int DSOLOCAL urldecode_nonstrict_inplace_ex(char *input, long int input_length, int *invalid_count); + +int DSOLOCAL html_entities_decode_inplace(apr_pool_t *mp, unsigned char *input, int len); + +int DSOLOCAL ansi_c_sequences_decode_inplace(unsigned char *input, int len); + +#endif diff --git a/apache2/msc_xml.c b/apache2/msc_xml.c new file mode 100644 index 00000000..a8f7025d --- /dev/null +++ b/apache2/msc_xml.c @@ -0,0 +1,137 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_xml.c,v 1.2 2006/12/04 20:04:09 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifdef WITH_LIBXML2 + +#include "msc_xml.h" + + +/** + * Initialise XML parser. + */ +int xml_init(modsec_rec *msr, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + + msr->xml = apr_pcalloc(msr->mp, sizeof(xml_data)); + if (msr->xml == NULL) return -1; + + return 1; +} + +#if 0 +static void xml_receive_sax_error(void *data, const char *msg, ...) { + modsec_rec *msr = (modsec_rec *)data; + char message[256]; + + if (msr == NULL) return; + + apr_snprintf(message, sizeof(message), "%s (line %i pos %i)", + log_escape_nq(msr->mp, msr->xml->parsing_ctx->lastError.message), + msr->xml->parsing_ctx->lastError.line, + msr->xml->parsing_ctx->lastError.int2); + + msr_log(msr, 5, "XML: Parsing error: %s", message); +} +#endif + +/** + * Feed one chunk of data to the XML parser. + */ +int xml_process_chunk(modsec_rec *msr, const char *buf, unsigned int size, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* We want to initialise our parsing context here, to + * enable us to pass it the first chunk of data so that + * it can attempt to auto-detect the encoding. + */ + if (msr->xml->parsing_ctx == NULL) { + + /* First invocation. */ + + msr_log(msr, 4, "XML: Initialising parser."); + + /* NOTE When Sax interface is used libxml will not + * create the document object, but we need it. + + msr->xml->sax_handler = (xmlSAXHandler *)apr_pcalloc(msr->mp, sizeof(xmlSAXHandler)); + if (msr->xml->sax_handler == NULL) return -1; + msr->xml->sax_handler->error = xml_receive_sax_error; + msr->xml->sax_handler->warning = xml_receive_sax_error; + msr->xml->parsing_ctx = xmlCreatePushParserCtxt(msr->xml->sax_handler, msr, + buf, size, "body.xml"); + + */ + + msr->xml->parsing_ctx = xmlCreatePushParserCtxt(NULL, NULL, buf, size, "body.xml"); + if (msr->xml->parsing_ctx == NULL) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed to create parsing context."); + return -1; + } + } else { + + /* Not a first invocation. */ + + xmlParseChunk(msr->xml->parsing_ctx, buf, size, 0); + if (msr->xml->parsing_ctx->wellFormed != 1) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed parsing document."); + return -1; + } + } + + return 1; +} + +/** + * Finalise XML parsing. + */ +int xml_complete(modsec_rec *msr, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* Only if we have a context, meaning we've done some work. */ + if (msr->xml->parsing_ctx != NULL) { + /* This is how we signalise the end of parsing to libxml. */ + xmlParseChunk(msr->xml->parsing_ctx, NULL, 0, 1); + + /* Preserve the results for our reference. */ + msr->xml->well_formed = msr->xml->parsing_ctx->wellFormed; + msr->xml->doc = msr->xml->parsing_ctx->myDoc; + + /* Clean up everything else. */ + xmlFreeParserCtxt(msr->xml->parsing_ctx); + msr->xml->parsing_ctx = NULL; + msr_log(msr, 4, "XML: Parsing complete (well_formed %i).", msr->xml->well_formed); + + if (msr->xml->well_formed != 1) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed parsing document."); + return -1; + } + } + + return 1; +} + +/** + * Frees the resources used for XML parsing. + */ +apr_status_t xml_cleanup(modsec_rec *msr) { + if (msr->xml->doc != NULL) { + xmlFreeDoc(msr->xml->doc); + msr->xml->doc = NULL; + } + + return 1; +} + +#endif diff --git a/apache2/msc_xml.h b/apache2/msc_xml.h new file mode 100644 index 00000000..cea27cef --- /dev/null +++ b/apache2/msc_xml.h @@ -0,0 +1,43 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: msc_xml.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_XML_H_ +#define _MSC_XML_H_ + +typedef struct xml_data xml_data; + +#include "modsecurity.h" +#include +#include + +/* Structures */ + +struct xml_data { + xmlSAXHandler *sax_handler; + xmlParserCtxtPtr parsing_ctx; + xmlDocPtr doc; + + unsigned int well_formed; +}; + +/* Functions */ + +int DSOLOCAL xml_init(modsec_rec *msr, char **error_msg); + +int DSOLOCAL xml_process_chunk(modsec_rec *msr, const char *buf, + unsigned int size, char **error_msg); + +int DSOLOCAL xml_complete(modsec_rec *msr, char **error_msg); + +apr_status_t DSOLOCAL xml_cleanup(modsec_rec *msr); + +#endif diff --git a/apache2/persist_dbm.c b/apache2/persist_dbm.c new file mode 100644 index 00000000..820e32e1 --- /dev/null +++ b/apache2/persist_dbm.c @@ -0,0 +1,514 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: persist_dbm.c,v 1.3 2006/12/21 19:57:41 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "persist_dbm.h" +#include "apr_sdbm.h" + +/** + * + */ +static apr_table_t *collection_unpack(modsec_rec *msr, char *blob, unsigned int blob_size, + int log_vars) +{ + apr_table_t *col = NULL; + unsigned int blob_offset; + + col = apr_table_make(msr->mp, 32); + if (col == NULL) return NULL; + + /* ENH verify the first 3 bytes (header) */ + + blob_offset = 3; + while (blob_offset + 1 < blob_size) { + msc_string *var = apr_pcalloc(msr->mp, sizeof(msc_string)); + + var->name_len = (blob[blob_offset] << 8) + blob[blob_offset + 1]; + if (var->name_len == 0) break; + + blob_offset += 2; + if (blob_offset + var->name_len > blob_size) return NULL; + var->name = apr_pstrmemdup(msr->mp, blob + blob_offset, var->name_len - 1); + blob_offset += var->name_len; + var->name_len--; + + var->value_len = (blob[blob_offset] << 8) + blob[blob_offset + 1]; + blob_offset += 2; + + if (blob_offset + var->value_len > blob_size) return NULL; + var->value = apr_pstrmemdup(msr->mp, blob + blob_offset, var->value_len - 1); + blob_offset += var->value_len; + var->value_len--; + + if (log_vars) { + msr_log(msr, 9, "Read variable: name \"%s\", value \"%s\".", + log_escape_ex(msr->mp, var->name, var->name_len), + log_escape_ex(msr->mp, var->value, var->value_len)); + } + + apr_table_addn(col, var->name, (void *)var); + } + + return col; +} + +/** + * + */ +apr_table_t *collection_retrieve(modsec_rec *msr, const char *col_name, + const char *col_key, int col_key_len) +{ + char *dbm_filename = NULL; + apr_status_t rc; + apr_sdbm_datum_t key; + apr_sdbm_datum_t *value = NULL; + apr_sdbm_t *dbm; + apr_table_t *col = NULL; + const apr_array_header_t *arr; + apr_table_entry_t *te; + int i; + + if (msr->txcfg->data_dir == NULL) { + msr_log(msr, 1, "Unable to retrieve collection (name \"%s\", key \"%s\"). Use " + "SecDataDir to define data directory first.", log_escape(msr->mp, col_name), + log_escape(msr->mp, col_key)); + return NULL; + } + + dbm_filename = apr_pstrcat(msr->mp, msr->txcfg->data_dir, "/", col_name, NULL); + + rc = apr_sdbm_open(&dbm, dbm_filename, APR_READ | APR_SHARELOCK, + CREATEMODE, msr->mp); + if (rc != APR_SUCCESS) { + return NULL; + } + + key.dptr = (char *)col_key; + key.dsize = col_key_len + 1; + + value = (apr_sdbm_datum_t *)apr_pcalloc(msr->mp, sizeof(apr_sdbm_datum_t)); + rc = apr_sdbm_fetch(dbm, value, key); + if (rc != APR_SUCCESS) { + apr_sdbm_close(dbm); + msr_log(msr, 1, "Failed to read from DBM file \"%s\": %s", log_escape(msr->mp, + dbm_filename), get_apr_error(msr->mp, rc)); + return NULL; + } + + if (value->dptr == NULL) { /* Key not found in DBM file. */ + apr_sdbm_close(dbm); + return NULL; + } + + /* Transform raw data into a table. */ + col = collection_unpack(msr, value->dptr, value->dsize, 1); + if (col == NULL) return NULL; + + /* Remove expired variables. */ + do { + arr = apr_table_elts(col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + if (strncmp(te[i].key, "__expire_", 9) == 0) { + msc_string *var = (msc_string *)te[i].val; + int expiry_time = atoi(var->value); + + /* Do not remove the record itself. */ + if (strcmp(te[i].key, "__expire_KEY") == 0) continue; + + if (expiry_time <= apr_time_sec(msr->request_time)) { + char *key_to_expire = apr_pstrdup(msr->mp, te[i].key); + msr_log(msr, 9, "Removing key \"%s\" from collection.", key_to_expire + 9); + apr_table_unset(col, key_to_expire + 9); + msr_log(msr, 9, "Removing key \"%s\" from collection.", key_to_expire); + apr_table_unset(col, key_to_expire); + msr_log(msr, 4, "Removed expired variable \"%s\".", key_to_expire + 9); + break; + } + } + } + } while(i != arr->nelts); + + + /* Update UPDATE_RATE */ + { + msc_string *var; + int create_time, counter; + + var = (msc_string *)apr_table_get(col, "CREATE_TIME"); + if (var == NULL) { + /* Error. */ + } else { + create_time = atoi(var->value); + var = (msc_string *)apr_table_get(col, "UPDATE_COUNTER"); + if (var == NULL) { + /* Error. */ + } else { + counter = atoi(var->value); + var = (msc_string *)apr_table_get(col, "UPDATE_RATE"); + if (var == NULL) { + var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "UPDATE_RATE"; + var->name_len = strlen(var->name); + apr_table_setn(col, var->name, (void *)var); + } + var->value = apr_psprintf(msr->mp, "%i", + (int)((60 * counter)/(apr_time_sec(apr_time_now()) - create_time))); + var->value_len = strlen(var->value); + } + } + } + + apr_sdbm_close(dbm); + + msr_log(msr, 4, "Retrieved collection (name \"%s\", key \"%s\").", + log_escape(msr->mp, col_name), log_escape(msr->mp, col_key)); + + return col; +} + +/** + * + */ +int collection_store(modsec_rec *msr, apr_table_t *col) { + char *dbm_filename = NULL; + msc_string *var_name = NULL, *var_key = NULL; + unsigned char *blob = NULL; + unsigned int blob_size, blob_offset; + apr_status_t rc; + apr_sdbm_datum_t key; + apr_sdbm_datum_t value; + apr_sdbm_t *dbm; + const apr_array_header_t *arr; + apr_table_entry_t *te; + int i; + + var_name = (msc_string *)apr_table_get(col, "__name"); + if (var_name == NULL) { + return -1; + } + + var_key = (msc_string *)apr_table_get(col, "__key"); + if (var_key == NULL) { + return -1; + } + + if (msr->txcfg->data_dir == NULL) { + msr_log(msr, 1, "Unable to store collection (name \"%s\", key \"%s\"). Use " + "SecDataDir to define data directory first.", + log_escape(msr->mp, var_name->value), log_escape(msr->mp, var_key->value)); + return -1; + } + + dbm_filename = apr_pstrcat(msr->mp, msr->txcfg->data_dir, "/", var_name->value, NULL); + + /* Remove expired variables. */ + do { + arr = apr_table_elts(col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + if (strncmp(te[i].key, "__expire_", 9) == 0) { + msc_string *var = (msc_string *)te[i].val; + int expiry_time = atoi(var->value); + + /* Do not remove the record itself. */ + if (strcmp(te[i].key, "__expire_KEY") == 0) continue; + + if (expiry_time <= apr_time_sec(msr->request_time)) { + char *key_to_expire = apr_pstrdup(msr->mp, te[i].key); + msr_log(msr, 9, "Removing key \"%s\" from collection.", key_to_expire + 9); + apr_table_unset(col, key_to_expire + 9); + msr_log(msr, 9, "Removing key \"%s\" from collection.", key_to_expire); + apr_table_unset(col, key_to_expire); + msr_log(msr, 4, "Removed expired variable \"%s\".", key_to_expire + 9); + break; + } + } + } + } while(i != arr->nelts); + + /* Delete the collection if the variable "KEY" does not exist. */ + if (apr_table_get(col, "KEY") == NULL) { + + rc = apr_sdbm_open(&dbm, dbm_filename, APR_CREATE | APR_WRITE | APR_SHARELOCK, + CREATEMODE, msr->mp); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed to access DBM file \"%s\": %s", + log_escape(msr->mp, dbm_filename), get_apr_error(msr->mp, rc)); + return -1; + } + + key.dptr = var_key->value; + key.dsize = var_key->value_len + 1; + + rc = apr_sdbm_delete(dbm, key); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed deleting collection (name \"%s\", " + "key \"%s\"): %s", log_escape(msr->mp, var_name->value), + log_escape(msr->mp, var_key->value), get_apr_error(msr->mp, rc)); + apr_sdbm_close(dbm); + return -1; + } + + msr_log(msr, 4, "Deleted collection (name \"%s\", key \"%s\").", + log_escape(msr->mp, var_name->value), log_escape(msr->mp, var_key->value)); + apr_sdbm_close(dbm); + + return 1; + } + + /* Update the timeout value. */ + { + msc_string *var = (msc_string *)apr_table_get(col, "TIMEOUT"); + if (var != NULL) { + int timeout = atoi(var->value); + var = (msc_string *)apr_table_get(col, "__expire_KEY"); + if (var != NULL) { + var->value = apr_psprintf(msr->mp, "%i", (int)(apr_time_sec(apr_time_now()) + timeout)); + var->value_len = strlen(var->value); + } + } + } + + /* LAST_UPDATE_TIME */ + { + msc_string *var = (msc_string *)apr_table_get(col, "LAST_UPDATE_TIME"); + if (var == NULL) { + var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "LAST_UPDATE_TIME"; + var->name_len = strlen(var->name); + apr_table_setn(col, var->name, (void *)var); + } + var->value = apr_psprintf(msr->mp, "%i", (int)(apr_time_sec(apr_time_now()))); + var->value_len = strlen(var->value); + } + + /* UPDATE_COUNTER */ + { + msc_string *var = (msc_string *)apr_table_get(col, "UPDATE_COUNTER"); + int counter = 0; + if (var == NULL) { + var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "UPDATE_COUNTER"; + var->name_len = strlen(var->name); + apr_table_setn(col, var->name, (void *)var); + } else { + counter = atoi(var->value); + } + var->value = apr_psprintf(msr->mp, "%i", counter + 1); + var->value_len = strlen(var->value); + } + + /* Calculate the size first. */ + blob_size = 3 + 2; + arr = apr_table_elts(col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *var = (msc_string *)te[i].val; + int len; + + len = var->name_len + 1; + if (len >= 65536) len = 65536; + blob_size += len + 2; + + len = var->value_len + 1; + if (len >= 65536) len = 65536; + blob_size += len + 2; + } + + /* Now generate the binary object. */ + blob = apr_pcalloc(msr->mp, blob_size); + if (blob == NULL) { + return -1; + } + + blob[0] = 0x49; + blob[1] = 0x52; + blob[2] = 0x01; + + blob_offset = 3; + arr = apr_table_elts(col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *var = (msc_string *)te[i].val; + int len; + + len = var->name_len + 1; + if (len >= 65536) len = 65536; + + blob[blob_offset + 0] = (len & 0xff00) >> 8; + blob[blob_offset + 1] = len & 0x00ff; + memcpy(blob + blob_offset + 2, var->name, len - 1); + blob[blob_offset + 2 + len - 1] = '\0'; + blob_offset += 2 + len; + + len = var->value_len + 1; + if (len >= 65536) len = 65536; + + blob[blob_offset + 0] = (len & 0xff00) >> 8; + blob[blob_offset + 1] = len & 0x00ff; + memcpy(blob + blob_offset + 2, var->value, len - 1); + blob[blob_offset + 2 + len - 1] = '\0'; + blob_offset += 2 + len; + + msr_log(msr, 9, "Wrote variable: name \"%s\", value \"%s\".", + log_escape_ex(msr->mp, var->name, var->name_len), + log_escape_ex(msr->mp, var->value, var->value_len)); + } + + blob[blob_offset] = 0; + blob[blob_offset + 1] = 0; + + /* And, finally, store it. */ + dbm_filename = apr_pstrcat(msr->mp, msr->txcfg->data_dir, "/", var_name->value, NULL); + + rc = apr_sdbm_open(&dbm, dbm_filename, APR_CREATE | APR_WRITE | APR_SHARELOCK, + CREATEMODE, msr->mp); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed to access DBM file \"%s\": %s", log_escape(msr->mp, dbm_filename), + get_apr_error(msr->mp, rc)); + return -1; + } + + key.dptr = var_key->value; + key.dsize = var_key->value_len + 1; + + value.dptr = (char *)blob; + value.dsize = blob_size; + + rc = apr_sdbm_store(dbm, key, value, APR_SDBM_REPLACE); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed to write to DBM file \"%s\": %s", dbm_filename, + get_apr_error(msr->mp, rc)); + apr_sdbm_close(dbm); + return -1; + } + + msr_log(msr, 4, "Persisted collection (name \"%s\", key \"%s\").", + log_escape(msr->mp, var_name->value), log_escape(msr->mp, var_key->value)); + + apr_sdbm_close(dbm); + + return 0; +} + +/** + * + */ +int collections_remove_stale(modsec_rec *msr, const char *col_name) { + char *dbm_filename = NULL; + apr_sdbm_datum_t key, value; + apr_sdbm_t *dbm; + apr_status_t rc; + apr_array_header_t *keys_arr; + char **keys; + int i; + unsigned int now = (unsigned int)apr_time_sec(msr->request_time); + + if (msr->txcfg->data_dir == NULL) { + /* The user has been warned about this problem enough times already by now. + * msr_log(msr, 1, "Unable to access collection file (name \"%s\"). Use SecDataDir to " + * "define data directory first.", log_escape(msr->mp, col_name)); + */ + return -1; + } + + dbm_filename = apr_pstrcat(msr->mp, msr->txcfg->data_dir, "/", col_name, NULL); + + rc = apr_sdbm_open(&dbm, dbm_filename, APR_CREATE | APR_WRITE | APR_SHARELOCK, + CREATEMODE, msr->mp); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed to access DBM file \"%s\": %s", log_escape(msr->mp, dbm_filename), + get_apr_error(msr->mp, rc)); + return -1; + } + + /* First get a list of all keys. */ + keys_arr = apr_array_make(msr->mp, 256, sizeof(char *)); + rc = apr_sdbm_lock(dbm, APR_FLOCK_SHARED); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed to lock DBM file \"%s\": %s", log_escape(msr->mp, dbm_filename), + get_apr_error(msr->mp, rc)); + apr_sdbm_close(dbm); + return -1; + } + + /* No one can write to the file while we're + * doing this so let's do it as fast as we can. + */ + rc = apr_sdbm_firstkey(dbm, &key); + while(rc == APR_SUCCESS) { + char *s = apr_pstrmemdup(msr->mp, key.dptr, key.dsize); + *(char **)apr_array_push(keys_arr) = s; + rc = apr_sdbm_nextkey(dbm, &key); + } + apr_sdbm_unlock(dbm); + + msr_log(msr, 9, "Found %i record(s) in file \"%s\".", keys_arr->nelts, + log_escape(msr->mp, dbm_filename)); + + /* Now retrieve the entires one by one. */ + keys = (char **)keys_arr->elts; + for (i = 0; i < keys_arr->nelts; i++) { + key.dptr = keys[i]; + key.dsize = strlen(key.dptr) + 1; + + rc = apr_sdbm_fetch(dbm, &value, key); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed reading DBM file \"%s\": %s", + log_escape(msr->mp, dbm_filename), get_apr_error(msr->mp, rc)); + apr_sdbm_close(dbm); + return -1; + } + + if (value.dptr != NULL) { + apr_table_t *col = NULL; + msc_string *var = NULL; + + col = collection_unpack(msr, value.dptr, value.dsize, 0); + if (col == NULL) { + return -1; + } + + var = (msc_string *)apr_table_get(col, "__expire_KEY"); + if (var == NULL) { + msr_log(msr, 1, "Collection cleanup discovered entry with no " + "__expire_KEY (name \"%s\", key \"%s\").", + log_escape(msr->mp, col_name), log_escape(msr->mp, key.dptr)); + } else { + unsigned int expiry_time = atoi(var->value); + + msr_log(msr, 9, "Record (name \"%s\", key \"%s\") set to expire in %i seconds.", + log_escape(msr->mp, col_name), log_escape(msr->mp, key.dptr), + expiry_time - now); + + if (expiry_time <= now) { + rc = apr_sdbm_delete(dbm, key); + if (rc != APR_SUCCESS) { + msr_log(msr, 1, "Failed deleting collection (name \"%s\", " + "key \"%s\"): %s", log_escape(msr->mp, col_name), + log_escape(msr->mp, key.dptr), get_apr_error(msr->mp, rc)); + return -1; + } + msr_log(msr, 4, "Removed stale collection (name \"%s\", " + "key \"%s\").", log_escape(msr->mp, col_name), + log_escape(msr->mp, key.dptr)); + } + } + } else { + /* Ignore entry not found - it may have been removed in the meantime. */ + } + } + + apr_sdbm_close(dbm); + + return 1; +} diff --git a/apache2/persist_dbm.h b/apache2/persist_dbm.h new file mode 100644 index 00000000..fe006a43 --- /dev/null +++ b/apache2/persist_dbm.h @@ -0,0 +1,26 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: persist_dbm.h,v 1.1.1.1 2006/10/14 09:30:43 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _PERSIST_DBM_H_ +#define _PERSIST_DBM_H_ + +#include "apr_general.h" +#include "modsecurity.h" + +apr_table_t DSOLOCAL *collection_retrieve(modsec_rec *msr, const char *col_name, + const char *col_value, int col_value_length); + +int DSOLOCAL collection_store(modsec_rec *msr, apr_table_t *collection); + +int DSOLOCAL collections_remove_stale(modsec_rec *msr, const char *col_name); + +#endif diff --git a/apache2/re.c b/apache2/re.c new file mode 100644 index 00000000..8e6a1bd7 --- /dev/null +++ b/apache2/re.c @@ -0,0 +1,1479 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re.c,v 1.15 2006/12/29 10:44:25 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "re.h" + + +/* -- Actions, variables, functions and operator functions ----------------- */ + +/** + * Creates msre_var instances (rule variables) out of the + * given text string and places them into the supplied table. + */ +apr_status_t msre_parse_targets(msre_ruleset *ruleset, const char *text, + apr_array_header_t *arr, char **error_msg) +{ + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + apr_table_t *vartable; + unsigned int count = 0; + apr_status_t rc; + msre_var *var; + int i; + + if (text == NULL) return -1; + + /* Extract name & value pairs first */ + vartable = apr_table_make(ruleset->mp, 10); + if (vartable == NULL) return -1; + rc = msre_parse_generic(ruleset->mp, text, vartable, error_msg); + if (rc < 0) return rc; + + /* Loop through the table and create variables */ + tarr = apr_table_elts(vartable); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + var = msre_create_var(ruleset, telts[i].key, telts[i].val, NULL, error_msg); + if (var == NULL) return -1; + *(msre_var **)apr_array_push(arr) = var; + count++; + } + + return count; +} + +/** + * Creates msre_action instances by parsing the given string, placing + * them into the supplied array. + */ +apr_status_t msre_parse_actions(msre_engine *engine, msre_actionset *actionset, + const char *text, char **error_msg) +{ + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + apr_table_t *vartable; + unsigned int count = 0; + apr_status_t rc; + msre_action *action; + int i; + + if (text == NULL) return -1; + + /* Extract name & value pairs first */ + vartable = apr_table_make(engine->mp, 10); + if (vartable == NULL) return -1; + rc = msre_parse_generic(engine->mp, text, vartable, error_msg); + if (rc < 0) return rc; + + /* Loop through the table and create actions */ + tarr = apr_table_elts(vartable); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + /* Create action. */ + action = msre_create_action(engine, telts[i].key, telts[i].val, error_msg); + if (action == NULL) return -1; + + /* Initialise action (option). */ + if (action->metadata->init != NULL) { + action->metadata->init(engine, actionset, action); + } + + if (action->metadata->cardinality == ACTION_CARDINALITY_ONE) { + /* One action per actionlist. */ + apr_table_setn(actionset->actions, action->metadata->name, (void *)action); + } else { + /* Multiple actions per actionlist. */ + apr_table_addn(actionset->actions, action->metadata->name, (void *)action); + } + + count++; + } + + return count; +} + +/** + * Locates variable metadata given the variable name. + */ +msre_var_metadata *msre_resolve_var(msre_engine *engine, const char *name) { + return (msre_var_metadata *)apr_table_get(engine->variables, name); +} + +/** + * Locates action metadata given the action name. + */ +msre_action_metadata *msre_resolve_action(msre_engine *engine, const char *name) { + return (msre_action_metadata *)apr_table_get(engine->actions, name); +} + +/** + * Creates a new variable instance given the variable name + * and an (optional) parameter. + */ +msre_var *msre_create_var_ex(msre_engine *engine, const char *name, const char *param, + modsec_rec *msr, char **error_msg) +{ + msre_var *var = apr_pcalloc(engine->mp, sizeof(msre_var)); + if (var == NULL) return NULL; + + if (error_msg == NULL) return NULL; + *error_msg = NULL; + + /* Handle negation and member counting */ + if (name[0] == '!') { + var->is_negated = 1; + var->name = name + 1; + } + else + if (name[0] == '&') { + var->is_counting = 1; + var->name = name + 1; + } + else { + var->name = name; + } + + /* CGI HTTP variables emulation. */ + if (strncasecmp(var->name, "HTTP_", 5) == 0) { + if (var->param != NULL) { + *error_msg = apr_psprintf(engine->mp, "Variable %s does not support parameters.", + var->name); + return NULL; + } + + var->param = var->name + 5; + var->name = "REQUEST_HEADERS"; + } + + /* Resolve variable */ + var->metadata = msre_resolve_var(engine, var->name); + if (var->metadata == NULL) { + *error_msg = apr_psprintf(engine->mp, "Unknown variable: %s", name); + return NULL; + } + + /* The counting operator "&" can only be used against collections. */ + if (var->is_counting) { + if (var->metadata->type == VAR_SIMPLE) { + *error_msg = apr_psprintf(engine->mp, "The & modificator does not apply to " + "non-collection variables."); + return NULL; + } + } + + /* Check the parameter. */ + if (param == NULL) { + if (var->metadata->argc_min > 0) { + *error_msg = apr_psprintf(engine->mp, "Missing mandatory parameter for variable %s.", + name); + return NULL; + } + } else { /* Parameter present */ + + /* Do we allow a parameter? */ + if (var->metadata->argc_max == 0) { + *error_msg = apr_psprintf(engine->mp, "Variable %s does not support parameters.", + name); + return NULL; + } + + var->param = param; + } + + return var; +} + +/** + * Create a new variable object from the provided name and value. + */ +msre_var *msre_create_var(msre_ruleset *ruleset, const char *name, const char *param, + modsec_rec *msr, char **error_msg) +{ + msre_var *var = msre_create_var_ex(ruleset->engine, name, param, msr, error_msg); + if (var == NULL) return NULL; + + /* Validate & initialise variable */ + if (var->metadata->validate != NULL) { + *error_msg = var->metadata->validate(ruleset, var); + if (*error_msg != NULL) { + /* ENH Shouldn't we log the problem? */ + return NULL; + } + } + + return var; +} + +/** + * Creates a new action instance given its name and an (optional) parameter. + */ +msre_action *msre_create_action(msre_engine *engine, const char *name, const char *param, + char **error_msg) +{ + msre_action *action = apr_pcalloc(engine->mp, sizeof(msre_action)); + if (action == NULL) return NULL; + + if (error_msg == NULL) return NULL; + *error_msg = NULL; + + /* Resolve action */ + action->metadata = msre_resolve_action(engine, name); + if (action->metadata == NULL) { + *error_msg = apr_psprintf(engine->mp, "Unknown action: %s", name); + return NULL; + } + + if (param == NULL) { /* Parameter not present */ + if (action->metadata->argc_min > 0) { + *error_msg = apr_psprintf(engine->mp, "Missing mandatory parameter for action %s", + name); + return NULL; + } + } else { /* Parameter present */ + + /* Should we allow the parameter? */ + if (action->metadata->argc_max == 0) { + *error_msg = apr_psprintf(engine->mp, "Extra parameter provided to action %s", name); + return NULL; + } + + /* Handle +/- modificators */ + if ((param[0] == '+')||(param[0] == '-')) { + if (action->metadata->allow_param_plusminus == 0) { + *error_msg = apr_psprintf(engine->mp, + "Action %s does not allow +/- modificators.", name); + return NULL; + } + else { /* Modificators allowed. */ + if (param[0] == '+') { + action->param = param + 1; + action->param_plusminus = POSITIVE_VALUE; + } else + if (param[0] == '-') { + action->param = param + 1; + action->param_plusminus = NEGATIVE_VALUE; + } + } + } else { + action->param = param; + } + + /* Validate parameter */ + if (action->metadata->validate != NULL) { + *error_msg = action->metadata->validate(engine, action); + if (*error_msg != NULL) return NULL; + } + } + + return action; +} + +/** + * Generic parser that is used as basis for target and action parsing. + * It breaks up the input string into name-parameter pairs and places + * them into the given table. + */ +int msre_parse_generic(apr_pool_t *mp, const char *text, apr_table_t *vartable, + char **error_msg) +{ + char *p = (char *)text; + int count = 0; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + count = 0; + while(*p != '\0') { + char *name = NULL, *value = NULL; + + /* ignore whitespace */ + while(isspace(*p)) p++; + if (*p == '\0') return count; + + /* we are at the beginning of the name */ + name = p; + while((*p != '\0')&&(*p != '|')&&(*p != ':')&&(*p != ',')&&(!isspace(*p))) p++; // ENH replace with isvarnamechar() + + /* get the name */ + name = apr_pstrmemdup(mp, name, p - name); + + if (*p != ':') { /* we don't have a parameter */ + /* add to the table with no value */ + apr_table_addn(vartable, name, NULL); + count++; + + /* go over any whitespace present */ + while(isspace(*p)) p++; + + /* we're done */ + if (*p == '\0') { + return count; + } + + /* skip over the separator character and continue */ + if ((*p == ',')||(*p == '|')) { + p++; + continue; + } + + *error_msg = apr_psprintf(mp, "Unexpected character at position %i: %s", + (int)(p - text), text); + return -1; + } + + /* we have a parameter */ + + p++; /* move over the colon */ + + /* we'll allow empty values */ + if (*p == '\0') { + apr_table_addn(vartable, name, NULL); + count++; + return count; + } + + if ((*p == ',')||(*p == '|')) { + apr_table_addn(vartable, name, NULL); + count++; + /* move over the separator char and continue */ + p++; + continue; + } + + /* we really have a parameter */ + + if (*p == '\'') { /* quoted value */ + char *d = NULL; + + p++; /* go over the openning quote */ + value = d = strdup(p); + if (d == NULL) return -1; + + for(;;) { + if (*p == '\0') { + *error_msg = apr_psprintf(mp, "Missing closing quote at position %i: %s", + (int)(p - text), text); + free(value); + return -1; + } else + if (*p == '\\') { + if ( (*(p + 1) == '\0') || ((*(p + 1) != '\'')&&(*(p + 1) != '\\')) ) { + *error_msg = apr_psprintf(mp, "Invalid quoted pair at position %i: %s", + (int)(p - text), text); + free(value); + return -1; + } + p++; + *d++ = *p++; + } else + if (*p == '\'') { + *d = '\0'; + p++; + break; + } + else { + *d++ = *p++; + } + } + + d = value; + value = apr_pstrdup(mp, d); + free(d); + } else { /* non-quoted value */ + value = p; + while((*p != '\0')&&(*p != ',')&&(*p != '|')&&(!isspace(*p))) p++; + value = apr_pstrmemdup(mp, value, p - value); + } + + /* add to table */ + apr_table_addn(vartable, name, value); + count++; + + /* move to the first character of the next name-value pair */ + while(isspace(*p)||(*p == ',')||(*p == '|')) p++; + } + + return count; +} + + +/* -- Actionset functions -------------------------------------------------- */ + +/** + * Creates an actionset instance and (as an option) populates it by + * parsing the given string which contains a list of actions. + */ +msre_actionset *msre_actionset_create(msre_engine *engine, const char *text, + char **error_msg) +{ + msre_actionset *actionset = (msre_actionset *)apr_pcalloc(engine->mp, + sizeof(msre_actionset)); + if (actionset == NULL) return NULL; + + actionset->actions = apr_table_make(engine->mp, 25); + if (actionset->actions == NULL) return NULL; + + /* Metadata */ + actionset->id = NOT_SET_P; + actionset->rev = NOT_SET_P; + actionset->msg = NOT_SET_P; + actionset->phase = NOT_SET; + actionset->severity = -1; + + /* Flow */ + actionset->is_chained = NOT_SET; + actionset->skip_count = NOT_SET; + + /* Disruptive */ + actionset->intercept_action = NOT_SET; + actionset->intercept_uri = NOT_SET_P; + actionset->intercept_status = NOT_SET; + actionset->intercept_pause = NOT_SET; + + /* Other */ + actionset->auditlog = NOT_SET; + actionset->log = NOT_SET; + + /* Parse the list of actions, if it's present */ + if (text != NULL) { + if (msre_parse_actions(engine, actionset, text, error_msg) < 0) { + return NULL; + } + } + + return actionset; +} + +/** + * Create a (shallow) copy of the supplied actionset. + */ +static msre_actionset *msre_actionset_copy(apr_pool_t *mp, msre_actionset *orig) { + msre_actionset *copy = NULL; + + if (orig == NULL) return NULL; + copy = (msre_actionset *)apr_pmemdup(mp, orig, sizeof(msre_actionset)); + if (copy == NULL) return NULL; + copy->actions = apr_table_copy(mp, orig->actions); + + return copy; +} + +/** + * Merges two actionsets into one. + */ +msre_actionset *msre_actionset_merge(msre_engine *engine, msre_actionset *parent, + msre_actionset *child, int inherit_by_default) +{ + msre_actionset *merged = NULL; + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + + if (inherit_by_default == 0) { + /* There is nothing to merge in this case. */ + return msre_actionset_copy(engine->mp, child); + } + + /* Start with a copy of the parent configuration. */ + merged = msre_actionset_copy(engine->mp, parent); + if (merged == NULL) return NULL; + + if (child == NULL) { + /* The child actionset does not exist, hence + * go with the parent one. + */ + return merged; + } + + /* First merge the hard-coded stuff. */ + + /* Metadata */ + if (child->id != NOT_SET_P) merged->id = child->id; + if (child->rev != NOT_SET_P) merged->rev = child->rev; + if (child->msg != NOT_SET_P) merged->msg = child->msg; + if (child->severity != NOT_SET) merged->severity = child->severity; + if (child->phase != NOT_SET) merged->phase = child->phase; + + /* Flow */ + merged->is_chained = child->is_chained; + if (child->skip_count != NOT_SET) merged->skip_count = child->skip_count; + + /* Disruptive */ + if (child->intercept_action != NOT_SET) { + merged->intercept_action = child->intercept_action; + merged->intercept_uri = child->intercept_uri; + } + + if (child->intercept_status != NOT_SET) merged->intercept_status = child->intercept_status; + if (child->intercept_pause != NOT_SET) merged->intercept_pause = child->intercept_pause; + + /* Other */ + if (child->auditlog != NOT_SET) merged->auditlog = child->auditlog; + if (child->log != NOT_SET) merged->log = child->log; + + + /* Now merge the actions. */ + + tarr = apr_table_elts(child->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + if (action->metadata->cardinality == ACTION_CARDINALITY_ONE) { + apr_table_setn(merged->actions, action->metadata->name, (void *)action); + } else { + apr_table_addn(merged->actions, action->metadata->name, (void *)action); + } + } + + return merged; +} + +/** + * Creates an actionset that contains a default list of actions. + */ +msre_actionset *msre_actionset_create_default(msre_engine *engine) { + char *my_error_msg = NULL; + return msre_actionset_create(engine, + "log,auditlog,deny,status:403,phase:2,t:lowercase,t:replaceNulls,t:compressWhitespace", + &my_error_msg); +} + +/** + * Sets the default values for the hard-coded actionset configuration. + */ +static void msre_actionset_set_defaults(msre_actionset *actionset) { + /* Metadata */ + if (actionset->id == NOT_SET_P) actionset->id = NULL; + if (actionset->rev == NOT_SET_P) actionset->rev = NULL; + if (actionset->msg == NOT_SET_P) actionset->msg = NULL; + if (actionset->phase == NOT_SET) actionset->phase = 2; + if (actionset->severity == -1); /* leave at -1 */ + + /* Flow */ + if (actionset->is_chained == NOT_SET) actionset->is_chained = 0; + if (actionset->skip_count == NOT_SET) actionset->skip_count = 0; + + /* Disruptive */ + if (actionset->intercept_action == NOT_SET) actionset->intercept_action = ACTION_NONE; + if (actionset->intercept_uri == NOT_SET_P) actionset->intercept_uri = NULL; + if (actionset->intercept_status == NOT_SET) actionset->intercept_status = 403; + if (actionset->intercept_pause == NOT_SET) actionset->intercept_pause = 0; + + /* Other */ + if (actionset->auditlog == NOT_SET) actionset->auditlog = 1; + if (actionset->log == NOT_SET) actionset->log = 1; +} + +/* -- Engine functions ----------------------------------------------------- */ + +/** + * Creates a new engine instance. + */ +msre_engine *msre_engine_create(apr_pool_t *parent_pool) { + msre_engine *engine; + apr_pool_t *mp; + + /* Create new memory pool */ + if (apr_pool_create(&mp, parent_pool) != APR_SUCCESS) return NULL; + + /* Init fields */ + engine = apr_pcalloc(mp, sizeof(msre_engine)); + if (engine == NULL) return NULL; + engine->mp = mp; + engine->tfns = apr_table_make(mp, 25); + if (engine->tfns == NULL) return NULL; + engine->operators = apr_table_make(mp, 25); + if (engine->operators == NULL) return NULL; + engine->variables = apr_table_make(mp, 25); + if (engine->variables == NULL) return NULL; + engine->actions = apr_table_make(mp, 25); + if (engine->actions == NULL) return NULL; + + return engine; +} + +/** + * Destroys an engine instance, releasing the consumed memory. + */ +void msre_engine_destroy(msre_engine *engine) { + /* Destroyed automatically by the parent pool. + * apr_pool_destroy(engine->mp); + */ +} + + +/* -- Recipe functions ----------------------------------------------------- */ + +#define NEXT_CHAIN 1 +#define NEXT_RULE 2 +#define SKIP_RULES 3 + +/** + * Default implementation of the ruleset phase processing; it processes + * the rules in the ruleset attached to the currently active + * transaction phase. + */ +apr_status_t msre_ruleset_process_phase(msre_ruleset *ruleset, modsec_rec *msr) { + apr_array_header_t *arr = NULL; + msre_rule **rules; + apr_status_t rc; + int i, mode, skip; + + /* First determine which set of rules we need to use. */ + switch (msr->phase) { + case PHASE_REQUEST_HEADERS : + arr = ruleset->phase_request_headers; + break; + case PHASE_REQUEST_BODY : + arr = ruleset->phase_request_body; + break; + case PHASE_RESPONSE_HEADERS : + arr = ruleset->phase_response_headers; + break; + case PHASE_RESPONSE_BODY : + arr = ruleset->phase_response_body; + break; + case PHASE_LOGGING : + arr = ruleset->phase_logging; + break; + default : + /* ENH Log a warning message here. */ + return -1; + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "This phase consists of %i rule(s).", arr->nelts); + } + + /* Loop through the rules in the selected set. */ + skip = 0; + mode = NEXT_RULE; + rules = (msre_rule **)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msre_rule *rule = rules[i]; + + /* NEXT_CHAIN is used when one of the rules in a chain + * fails to match and then we need to skip the remaining + * rules in that chain in order to get to the next + * rule that can execute. + */ + if (mode == NEXT_CHAIN) { + if (rule->actionset->is_chained == 0) { + mode = NEXT_RULE; + } + + /* Go to the next rule. */ + continue; + } + + /* If we are here that means the mode is NEXT_RULE, which + * then means we have done processing any chains. However, + * if the "skip" parameter is set we need to skip over. + */ + if ((mode == NEXT_RULE)&&(skip > 0)) { + /* Decrement the skip counter by one. */ + skip--; + + /* If the current rule is part of a chain then + * we need to skip over the entire chain. Thus + * we change the mode to NEXT_CHAIN. The skip + * counter will not decrement as we are moving + * over the rules belonging to the chain. + */ + if (rule->actionset->is_chained) { + mode = NEXT_CHAIN; + } + + /* Go to the next rule. */ + continue; + } + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Recipe: Invoking rule %x.", rule); + } + + rc = msre_rule_process(rule, msr); + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Rule returned %i.", rc); + } + + if (rc == RULE_NO_MATCH) { + if (rule->actionset->is_chained) { + /* If the current rule is part of a chain then + * we need to skip over all the rules in the chain. + */ + mode = NEXT_CHAIN; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "No match, chained -> mode NEXT_CHAIN."); + } + } else { + /* This rule is not part of a chain so we simply + * move to the next rule. + */ + mode = NEXT_RULE; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "No match, not chained -> mode NEXT_RULE."); + } + } + } + else + if (rc == RULE_MATCH) { + if (msr->was_intercepted) { + /* If the transaction was intercepted we will + * go back. Do note that we are relying on the + * rule to know if it is a part of a chain and + * not intercept if it is. + */ + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Match, intercepted -> returning."); + } + return 1; + } + + /* We had a match but the transaction was not + * intercepted. In that case we proceed with the + * next rule... + */ + mode = NEXT_RULE; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Match -> mode NEXT_RULE."); + } + + /* ...unless we need to skip, in which case we + * determine how many rules/chains we need to + * skip and configure the counter accordingly. + */ + if (rule->actionset->is_chained == 0) { + if (rule->chain_starter != NULL) { + if (rule->chain_starter->actionset->skip_count > 0) { + skip = rule->chain_starter->actionset->skip_count; + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Skipping %i rules/chains (from a chain).", skip); + } + } + } + else if (rule->actionset->skip_count > 0) { + skip = rule->actionset->skip_count; + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Skipping %i rules/chains.", skip); + } + } + } + } + else { + msr_log(msr, 1, "Unknown rule processing return code: %i.", rc); + return -1; + } + } + + /* ENH warn if chained rules are missing. */ + + return 0; +} + +/** + * Creates a ruleset that will be handled by the default + * implementation. + */ +msre_ruleset *msre_ruleset_create(msre_engine *engine, apr_pool_t *mp) { + msre_ruleset *ruleset; + + ruleset = apr_pcalloc(mp, sizeof(msre_ruleset)); + if (ruleset == NULL) return NULL; + ruleset->mp = mp; + ruleset->engine = engine; + + ruleset->phase_request_headers = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *)); + ruleset->phase_request_body = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *)); + ruleset->phase_response_headers = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *)); + ruleset->phase_response_body = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *)); + ruleset->phase_logging = apr_array_make(ruleset->mp, 25, sizeof(const msre_rule *)); + + return ruleset; +} + +/** + * Adds one rule to the given phase of the ruleset. + */ +int msre_ruleset_rule_add(msre_ruleset *ruleset, msre_rule *rule, int phase) { + apr_array_header_t *arr = NULL; + + switch (phase) { + case PHASE_REQUEST_HEADERS : + arr = ruleset->phase_request_headers; + break; + case PHASE_REQUEST_BODY : + arr = ruleset->phase_request_body; + break; + case PHASE_RESPONSE_HEADERS : + arr = ruleset->phase_response_headers; + break; + case PHASE_RESPONSE_BODY : + arr = ruleset->phase_response_body; + break; + case PHASE_LOGGING : + arr = ruleset->phase_logging; + break; + default : + return -1; + } + + /* ENH verify the rule's use of targets is consistent with + * the phase it selected to run at. + */ + + msre_actionset_set_defaults(rule->actionset); + *(const msre_rule **)apr_array_push(arr) = rule; + + return 1; +} + +static int msre_ruleset_phase_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re, + apr_array_header_t *phase_arr) +{ + msre_rule **rules; + int i, j, mode, removed_count; + + j = 0; + mode = 0; + removed_count = 0; + rules = (msre_rule **)phase_arr->elts; + for (i = 0; i < phase_arr->nelts; i++) { + msre_rule *rule = (msre_rule *)rules[i]; + + if (mode == 0) { /* Looking for next rule. */ + int remove_rule = 0; + + switch(re->type) { + case RULE_EXCEPTION_REMOVE_ID : + if ((rule->actionset != NULL)&&(rule->actionset->id != NULL)) { + int ruleid = atoi(rule->actionset->id); + + if (rule_id_in_range(ruleid, re->param)) { + remove_rule = 1; + } + } + + break; + + case RULE_EXCEPTION_REMOVE_MSG : + if ((rule->actionset != NULL)&&(rule->actionset->msg != NULL)) { + char *my_error_msg = NULL; + + int rc = msc_regexec(re->param_data, + rule->actionset->msg, strlen(rule->actionset->msg), + &my_error_msg); + if (rc >= 0) { + remove_rule = 1; + } + } + + break; + } + + if (remove_rule) { + /* Do not increment j. */ + removed_count++; + if (rule->actionset->is_chained) mode = 2; /* Remove rules in this chain. */ + } else { + if (rule->actionset->is_chained) mode = 1; /* Keep rules in this chain. */ + rules[j++] = rules[i]; + } + } else { /* Handling rule that is part of a chain. */ + if (mode == 2) { /* We want to remove the rule. */ + /* Do not increment j. */ + removed_count++; + } else { + rules[j++] = rules[i]; + } + + if ((rule->actionset == NULL)||(rule->actionset->is_chained == 0)) mode = 0; + } + } + + /* Update the number of rules in the array. */ + phase_arr->nelts -= removed_count; + + return 0; +} + +/** + * Removes from the ruleset all rules that match the given exception. + */ +int msre_ruleset_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re) { + int count = 0; + + if (ruleset == NULL) return 0; + + count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_request_headers); + count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_request_body); + count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_response_headers); + count += msre_ruleset_phase_rule_remove_with_exception(ruleset, re, ruleset->phase_request_body); + + return count; +} + + +/* -- Rule functions ------------------------------------------------------- */ + +/** + * Returns the name of the supplied severity level. + */ +static const char *msre_format_severity(int severity) { + static const char *const severities[] = { + "EMERGENCY", + "ALERT", + "CRITICAL", + "ERROR", + "WARNING", + "NOTICE", + "INFO", + "DEBUG", + NULL, + }; + + if ((severity >= 0)&&(severity <= 7)) { + return severities[severity]; + } + else { + return "(invalid value)"; + } +} + +/** + * Creates a string containing the metadata of the supplied rule. + */ +char *msre_format_metadata(modsec_rec *msr, msre_actionset *actionset) { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + char *id = ""; + char *rev = ""; + char *msg = ""; + char *severity = ""; + char *tags = ""; + int k; + + if (actionset == NULL) return ""; + + if (actionset->id != NULL) id = apr_psprintf(msr->mp, " [id \"%s\"]", + log_escape(msr->mp, actionset->id)); + if (actionset->rev != NULL) rev = apr_psprintf(msr->mp, " [rev \"%s\"]", + log_escape(msr->mp, actionset->rev)); + if (actionset->msg != NULL) { + /* Expand variables in the message string. */ + msc_string *var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->value = (char *)actionset->msg; + var->value_len = strlen(actionset->msg); + expand_macros(msr, var, NULL, msr->mp); + + msg = apr_psprintf(msr->mp, " [msg \"%s\"]", + log_escape_ex(msr->mp, var->value, var->value_len)); + } + if ((actionset->severity >= 0)&&(actionset->severity <= 7)) { + severity = apr_psprintf(msr->mp, " [severity \"%s\"]", + msre_format_severity(actionset->severity)); + } + + /* Extract rule tags from the action list. */ + tarr = apr_table_elts(actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + + for (k = 0; k < tarr->nelts; k++) { + msre_action *action = (msre_action *)telts[k].val; + if (strcmp(telts[k].key, "tag") == 0) { + tags = apr_psprintf(msr->mp, "%s [tag \"%s\"]", tags, + log_escape(msr->mp, action->param)); + } + } + + return apr_pstrcat(msr->mp, id, rev, msg, severity, tags, NULL); +} + +/** + * Assembles a new rule using the strings that contain a list + * of targets (variables), argumments, and actions. + */ +msre_rule *msre_rule_create(msre_ruleset *ruleset, const char *targets, + const char *args, const char *actions, char **error_msg) +{ + msre_rule *rule; + char *my_error_msg; + const char *argsp; + int rc; + + if (error_msg == NULL) return NULL; + *error_msg = NULL; + + rule = (msre_rule *)apr_pcalloc(ruleset->mp, sizeof(msre_rule)); + if (rule == NULL) return NULL; + rule->ruleset = ruleset; + rule->targets = apr_array_make(ruleset->mp, 10, sizeof(const msre_var *)); + + /* Parse targets */ + rc = msre_parse_targets(ruleset, targets, rule->targets, &my_error_msg); + if (rc < 0) { + *error_msg = apr_psprintf(ruleset->mp, "Error creating rule: %s", my_error_msg); + return NULL; + } + + /* Parse args */ + argsp = args; + + /* Is negation used? */ + if (*argsp == '!') { + rule->op_negated = 1; + argsp++; + while((isspace(*argsp))&&(*argsp != '\0')) argsp++; + } + + /* Is the operator explicitly selected? */ + if (*argsp != '@') { + /* Go with a regular expression. */ + rule->op_name = "rx"; + rule->op_param = argsp; + } else { + /* Explicitly selected operator. */ + char *p = (char *)(argsp + 1); + while((!isspace(*p))&&(*p != '\0')) p++; + rule->op_name = apr_pstrmemdup(ruleset->mp, argsp + 1, p - (argsp + 1)); + while(isspace(*p)) p++; /* skip over the whitespace at the end*/ + rule->op_param = p; /* IMP1 So we always have a parameter even when it's empty? */ + } + + /* Find the operator. */ + rule->op_metadata = msre_engine_op_resolve(ruleset->engine, rule->op_name); + if (rule->op_metadata == NULL) { + *error_msg = apr_psprintf(ruleset->mp, + "Error creating rule: Failed to resolve operator: %s", rule->op_name); + return NULL; + } + + /* Initialise & validate parameter */ + if (rule->op_metadata->param_init != NULL) { + if (rule->op_metadata->param_init(rule, &my_error_msg) <= 0) { + *error_msg = apr_psprintf(ruleset->mp, "Error creating rule: %s", my_error_msg); + return NULL; + } + } + + /* Parse actions */ + if (actions != NULL) { + /* Create per-rule actionset */ + rule->actionset = msre_actionset_create(ruleset->engine, actions, &my_error_msg); + if (rule->actionset == NULL) { + *error_msg = apr_psprintf(ruleset->mp, "Error parsing actions: %s", my_error_msg); + return NULL; + } + } + + return rule; +} + +/** + * Perform non-disruptive actions associated with the provided actionset. + */ +static void msre_perform_nondisruptive_actions(modsec_rec *msr, msre_rule *rule, + msre_actionset *actionset, apr_pool_t *mptmp) +{ + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + + tarr = apr_table_elts(actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + if (action->metadata->type == ACTION_NON_DISRUPTIVE) { + if (action->metadata->execute != NULL) { + action->metadata->execute(msr, mptmp, rule, action); + } + } + } +} + +/** + * Perform the disruptive actions associated with the given actionset. + */ +static void msre_perform_disruptive_actions(modsec_rec *msr, msre_rule *rule, + msre_actionset *actionset, apr_pool_t *mptmp, const char *message) +{ + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + + /* Execute the disruptive actions. Do note that this does + * not mean the request will be interrupted straight away. All + * disruptive actions need to do here is update the information + * that will be used to act later. + */ + tarr = apr_table_elts(actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + if (action->metadata->type == ACTION_DISRUPTIVE) { + if (action->metadata->execute != NULL) { + action->metadata->execute(msr, mptmp, rule, action); + } + } + } + + /* If "noauditlog" was used do not mark the transaction for audit logging. */ + if (actionset->auditlog == 1) { + msr->is_relevant++; + } + + /* We only do stuff when in ONLINE mode. In all other + * cases we only emit warnings. + */ + if ((msr->phase == PHASE_LOGGING) + || (msr->txcfg->is_enabled == MODSEC_DETECTION_ONLY) + || (msr->modsecurity->processing_mode == MODSEC_OFFLINE) + || (actionset->intercept_action == ACTION_NONE)) + { + /* If "nolog" was used log at a higher level. */ + msc_alert(msr, (actionset->log == 0 ? 4 : 2), actionset, + "Warning.", message); + return; + } + + /* Signal to the engine we need to intercept this + * transaction, and rememer the rule that caused it. + */ + msr->was_intercepted = 1; + msr->intercept_phase = msr->phase; + msr->intercept_actionset = actionset; + msr->intercept_message = message; +} + +/** + * Invokes the rule operator against the given value. + */ +static int execute_operator(msre_var *var, msre_rule *rule, modsec_rec *msr, + msre_actionset *acting_actionset, apr_pool_t *mptmp) +{ + apr_time_t time_before_regex; + char *my_error_msg = NULL; + int rc; + + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Executing operator %s%s with param \"%s\" against %s.", + (rule->op_negated ? "!" : ""), rule->op_name, + log_escape(msr->mp, rule->op_param), var->name); + } + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Target value: %s", log_escape_nq_ex(msr->mp, var->value, + var->value_len)); + } + + time_before_regex = apr_time_now(); /* IMP1 time_before_regex? */ + rc = rule->op_metadata->execute(msr, rule, var, &my_error_msg); + if (msr->txcfg->debuglog_level >= 4) { + msr_log(msr, 4, "Operator completed in %" APR_TIME_T_FMT " usec.", + (apr_time_now() - time_before_regex)); + } + + if (rc < 0) { + msr_log(msr, 4, "Operator error: %s", my_error_msg); + return -1; + } + + if (((rc == 0)&&(rule->op_negated == 0))||((rc == 1)&&(rule->op_negated == 1))) { + /* No match, do nothing. */ + return RULE_NO_MATCH; + } + else { + /* Match. */ + + if (rc == 0) { + /* Operator did not match so we need to provide a message. */ + my_error_msg = apr_psprintf(msr->mp, "Match of \"%s %s\" against \"%s\" required.", + log_escape(msr->mp, rule->op_name), log_escape(msr->mp, rule->op_param), + log_escape(msr->mp, var->name)); + } + + msr->matched_var = apr_pstrdup(msr->mp, var->name); + + /* Perform non-disruptive actions. */ + msre_perform_nondisruptive_actions(msr, rule, rule->actionset, mptmp); + + /* Perform disruptive actions, but only if + * this rule is not part of a chain. + */ + if (rule->actionset->is_chained == 0) { + msre_perform_disruptive_actions(msr, rule, acting_actionset, mptmp, my_error_msg); + } + + return RULE_MATCH; + } +} + +/** + * Executes rule against the given transaction. + */ +apr_status_t msre_rule_process(msre_rule *rule, modsec_rec *msr) { + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + msre_actionset *acting_actionset = NULL; + msre_var **targets = NULL; + apr_pool_t *mptmp = NULL; + apr_table_t *tartab = NULL; + apr_table_t *vartab = NULL; + int i, rc, match_count = 0; + int invocations = 0; + int multi_match = 0; + + /* Choose the correct metadata/disruptive action actionset. */ + acting_actionset = rule->actionset; + if (rule->chain_starter != NULL) { + acting_actionset = rule->chain_starter->actionset; + } + + /* Configure recursive matching. */ + if (apr_table_get(rule->actionset->actions, "multiMatch") != NULL) { + multi_match = 1; + } + + /* Create a memory pool that will be used during the + * processing of this rule only. + */ + /* IMP1 Why not have one pool and just clear it between rules? */ + if (apr_pool_create(&mptmp, NULL) != APR_SUCCESS) { + return -1; + } + + tartab = apr_table_make(mptmp, 24); + if (tartab == NULL) return -1; + vartab = apr_table_make(mptmp, 24); + if (vartab == NULL) return -1; + + /* Expand variables to create a list of targets. */ + + targets = (msre_var **)rule->targets->elts; + for (i = 0; i < rule->targets->nelts; i++) { + int j, list_count; + + apr_table_clear(vartab); + + /* ENH Introduce a new variable hook that would allow the code + * behind the variable to return the size of the collection + * without having to generate the variables. + */ + + /* Expand individual variables first. */ + list_count = targets[i]->metadata->generate(msr, targets[i], rule, vartab, mptmp); + + if (targets[i]->is_counting) { + /* Count how many there are and just add the score to the target list. */ + msre_var *newvar = (msre_var *)apr_pmemdup(mptmp, targets[i], sizeof(msre_var)); + newvar->value = apr_psprintf(mptmp, "%i", list_count); + newvar->value_len = strlen(newvar->value); + apr_table_addn(tartab, newvar->name, (void *)newvar); + } else { + /* And either add them or remove from the final target list. */ + arr = apr_table_elts(vartab); + te = (apr_table_entry_t *)arr->elts; + for(j = 0; j < arr->nelts; j++) { + if (targets[i]->is_negated == 0) { + apr_table_addn(tartab, te[j].key, te[j].val); + } else { + apr_table_unset(tartab, te[j].key); + } + } + } + } + + /* Loop through targets on the final target list, + * perform transformations as necessary, and invoke + * the operator. + */ + + arr = apr_table_elts(tartab); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int changed; + + /* Take one target. */ + msre_var *var = (msre_var *)te[i].val; + + /* Transform target. */ + { + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + apr_table_t *normtab; + int k; + msre_action *action; + msre_tfn_metadata *metadata; + + normtab = apr_table_make(mptmp, 10); + if (normtab == NULL) return -1; + tarr = apr_table_elts(rule->actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + + /* Build the final list of transformation functions. */ + for (k = 0; k < tarr->nelts; k++) { + action = (msre_action *)telts[k].val; + if (strcmp(telts[k].key, "t") == 0) { + if (strcmp(action->param, "none") == 0) { + apr_table_clear(normtab); + continue; + } + + if (action->param_plusminus == NEGATIVE_VALUE) { + apr_table_unset(normtab, action->param); + } else { + apr_table_addn(normtab, action->param, (void *)action); + } + } + } + + /* Perform transformations. */ + + tarr = apr_table_elts(normtab); + + /* Make a copy of the variable value so that + * we can change it in-place. + */ + if (tarr->nelts) { + var->value = apr_pstrmemdup(mptmp, var->value, var->value_len); + /* var->value_len remains the same */ + } + + /* Execute transformations in a loop. */ + + changed = 1; + telts = (const apr_table_entry_t*)tarr->elts; + for (k = 0; k < tarr->nelts; k++) { + char *rval = NULL; + long int rval_length = -1; + + /* In multi-match mode we execute the operator + * once at the beginning and then once every + * time the variable is changed by the transformation + * function. + */ + if (multi_match && changed) { + invocations++; + + rc = execute_operator(var, rule, msr, acting_actionset, mptmp); + + if (rc < 0) { + apr_pool_destroy(mptmp); + return -1; + } + + if (rc == RULE_MATCH) { + match_count++; + + /* Return straight away if the transaction + * was intercepted - no need to process the remaining + * targets. + */ + if (msr->was_intercepted) { + return RULE_MATCH; + } + } + } + + /* Perform one transformation. */ + action = (msre_action *)telts[k].val; + metadata = (msre_tfn_metadata *)action->param_data; + + rc = metadata->execute(mptmp, (char *)var->value, var->value_len, + &rval, &rval_length); + if (rc < 0) { + apr_pool_destroy(mptmp); + return -1; + } + + changed = rc; + + var->value = rval; + var->value_len = rval_length; + + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "T (%i) %s: %s", rc, metadata->name, + log_escape_nq_ex(mptmp, var->value, var->value_len)); + } + } + } + + /* Execute operator if multi-matching is not enabled, + * or if it is and we need to process the result of the + * last transformation. + */ + if (!multi_match || changed) { + invocations++; + + rc = execute_operator(var, rule, msr, acting_actionset, mptmp); + + if (rc < 0) { + apr_pool_destroy(mptmp); + return -1; + } + + if (rc == RULE_MATCH) { + match_count++; + + /* Return straight away if the transaction + * was intercepted - no need to process the remaining + * targets. + */ + if (msr->was_intercepted) { + return RULE_MATCH; + } + } + } + } + + apr_pool_destroy(mptmp); + + return (match_count ? RULE_MATCH : RULE_NO_MATCH); +} + +/** + * Checks whether the given rule ID is in the given range. + */ +int rule_id_in_range(int ruleid, const char *range) { + char *p = NULL, *saveptr = NULL; + char *data = NULL; + + if (range == NULL) return 0; + data = strdup(range); + if (data == NULL) return 0; + + p = apr_strtok(data, ",", &saveptr); + while(p != NULL) { + char *s = strstr(p, "-"); + if (s == NULL) { + if (ruleid == atoi(p)) { + free(data); + return 1; + } + } else { + int start = atoi(p); + int end = atoi(s + 1); + if ((ruleid >= start)&&(ruleid <= end)) { + free(data); + return 1; + } + } + p = apr_strtok(NULL, ",", &saveptr); + } + + free(data); + + return 0; +} diff --git a/apache2/re.h b/apache2/re.h new file mode 100644 index 00000000..9a9ac660 --- /dev/null +++ b/apache2/re.h @@ -0,0 +1,299 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re.h,v 1.7 2006/12/29 10:31:38 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#ifndef _MSC_RE_H_ +#define _MSC_RE_H_ + +#define ABSOLUTE_VALUE 0 +#define POSITIVE_VALUE 1 +#define NEGATIVE_VALUE 2 + +typedef struct msre_engine msre_engine; +typedef struct msre_ruleset msre_ruleset; +typedef struct msre_ruleset_internal msre_ruleset_internal; +typedef struct msre_rule msre_rule; +typedef struct msre_var_metadata msre_var_metadata; +typedef struct msre_var msre_var; +typedef struct msre_op_metadata msre_op_metadata; +typedef struct msre_tfn_metadata msre_tfn_metadata; +typedef struct msre_actionset msre_actionset; +typedef struct msre_action_metadata msre_action_metadata; +typedef struct msre_action msre_action; + +#include "apr_general.h" +#include "apr_tables.h" +#include "modsecurity.h" +#include "msc_pcre.h" +#include "persist_dbm.h" +#include "apache2.h" + + +/* Actions, variables, functions and operator functions */ + +int DSOLOCAL expand_macros(modsec_rec *msr, msc_string *var, msre_rule *rule, apr_pool_t *mptmp); + +apr_status_t msre_parse_targets(msre_ruleset *ruleset, const char *text, + apr_array_header_t *arr, char **error_msg); + +apr_status_t msre_parse_actions(msre_engine *engine, msre_actionset *actionset, + const char *text, char **error_msg); + +msre_var_metadata *msre_resolve_var(msre_engine *engine, const char *name); + +msre_action_metadata *msre_resolve_action(msre_engine *engine, const char *name); + +msre_var *msre_create_var(msre_ruleset *ruleset, const char *name, const char *param, + modsec_rec *msr, char **error_msg); + +msre_var *msre_create_var_ex(msre_engine *engine, const char *name, const char *param, + modsec_rec *msr, char **error_msg); + +msre_action *msre_create_action(msre_engine *engine, const char *name, + const char *param, char **error_msg); + +int msre_parse_generic(apr_pool_t *pool, const char *text, apr_table_t *vartable, + char **error_msg); + +int DSOLOCAL rule_id_in_range(int ruleid, const char *range); + + +/* Structures with the corresponding functions */ + +struct msre_engine { + apr_pool_t *mp; + apr_table_t *variables; + apr_table_t *operators; + apr_table_t *actions; + apr_table_t *tfns; +}; + +msre_engine *msre_engine_create(apr_pool_t *parent_pool); + +void msre_engine_destroy(msre_engine *engine); + +msre_op_metadata *msre_engine_op_resolve(msre_engine *engine, const char *name); + +struct msre_ruleset { + apr_pool_t *mp; + msre_engine *engine; + + apr_array_header_t *phase_request_headers; + apr_array_header_t *phase_request_body; + apr_array_header_t *phase_response_headers; + apr_array_header_t *phase_response_body; + apr_array_header_t *phase_logging; +}; + +apr_status_t msre_ruleset_process_phase(msre_ruleset *ruleset, modsec_rec *msr); + +apr_status_t msre_ruleset_process_phase_internal(msre_ruleset *ruleset, modsec_rec *msr); + +msre_ruleset *msre_ruleset_create(msre_engine *engine, apr_pool_t *mp); + +int msre_ruleset_rule_add(msre_ruleset *ruleset, msre_rule *rule, int phase); + +int DSOLOCAL msre_ruleset_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re); + +/* +int msre_ruleset_phase_rule_remove_with_exception(msre_ruleset *ruleset, rule_exception *re, + apr_array_header_t *phase_arr); +*/ + +#define RULE_NO_MATCH 0 +#define RULE_MATCH 1 + +struct msre_rule { + apr_array_header_t *targets; + const char *op_name; + const char *op_param; + void *op_param_data; + msre_op_metadata *op_metadata; + unsigned int op_negated; + msre_actionset *actionset; + + msre_ruleset *ruleset; + msre_rule *chain_starter; +}; + +msre_rule *msre_rule_create(msre_ruleset *ruleset, const char *targets, + const char *args, const char *actions, char **error_msg); + +void msre_rule_actionset_init(msre_rule *rule); + +apr_status_t msre_rule_process(msre_rule *rule, modsec_rec *msr); + +#define VAR_SIMPLE 0 /* REQUEST_URI */ +#define VAR_LIST 1 + +#define PHASE_REQUEST_HEADERS 1 +#define PHASE_REQUEST_BODY 2 +#define PHASE_RESPONSE_HEADERS 3 +#define PHASE_RESPONSE_BODY 4 +#define PHASE_LOGGING 5 + +#define FN_OP_PARAM_INIT(X) int (*X)(msre_rule *rule, char **error_msg) +#define FN_OP_EXECUTE(X) int (*X)(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) + + +struct msre_op_metadata { + const char *name; + FN_OP_PARAM_INIT (param_init); + FN_OP_EXECUTE (execute); +}; + +#define FN_TFN_EXECUTE(X) int (*X)(apr_pool_t *pool, unsigned char *input, long int input_length, char **rval, long int *rval_length) + +struct msre_tfn_metadata { + const char *name; + + /* Functions should populate *rval and return 1 on + * success, or return -1 on failure (in which case *rval + * should contain the error message. Strict functions + * (those that validate in + * addition to transforming) can return 0 when input + * fails validation. Functions are free to perform + * in-place transformation, or to allocate a new buffer + * from the provideded temporary (per-rule) memory pool. + * + * NOTE Strict transformation functions not supported yet. + */ + FN_TFN_EXECUTE(execute); +}; + +void DSOLOCAL msre_engine_tfn_register(msre_engine *engine, const char *name, + FN_TFN_EXECUTE(execute)); + +void msre_engine_op_register(msre_engine *engine, const char *name, + FN_OP_PARAM_INIT(fn1), FN_OP_EXECUTE(fn2)); + +void DSOLOCAL msre_engine_register_default_tfns(msre_engine *engine); + +void DSOLOCAL msre_engine_register_default_variables(msre_engine *engine); + +void DSOLOCAL msre_engine_register_default_operators(msre_engine *engine); + +void DSOLOCAL msre_engine_register_default_actions(msre_engine *engine); + +msre_tfn_metadata DSOLOCAL *msre_engine_tfn_resolve(msre_engine *engine, const char *name); + +#define VAR_DONT_CACHE 0 +#define VAR_CACHE 1 + +#define FN_VAR_VALIDATE(X) char *(*X)(msre_ruleset *ruleset, msre_var *var) +#define FN_VAR_GENERATE(X) int (*X)(modsec_rec *msr, msre_var *var, msre_rule *rule, apr_table_t *table, apr_pool_t *mptmp) + +struct msre_var_metadata { + const char *name; + unsigned int type; /* VAR_TYPE_ constants */ + unsigned int argc_min; + unsigned int argc_max; + FN_VAR_VALIDATE (validate); + FN_VAR_GENERATE (generate); + unsigned int is_cacheable; /* 0 - no, 1 - yes */ + unsigned int availability; /* when does this variable become available? */ +}; + +struct msre_var { + const char *name; + const char *value; + unsigned int value_len; + const char *param; + const void *param_data; + msre_var_metadata *metadata; + msc_regex_t *param_regex; + unsigned int is_negated; + unsigned int is_counting; +}; + + +struct msre_actionset { + apr_table_t *actions; + + /* Metadata */ + const char *id; + const char *rev; + const char *msg; + int severity; + int phase; + + /* Flow */ + int is_chained; + int skip_count; + + /* Disruptive */ + int intercept_action; + const char *intercept_uri; + int intercept_status; + int intercept_pause; + + /* Other */ + int log; + int auditlog; +}; + +msre_actionset *msre_actionset_create(msre_engine *engine, const char *text, + char **error_msg); + +msre_actionset *msre_actionset_merge(msre_engine *engine, msre_actionset *parent, + msre_actionset *child, int inherit_by_default); + +msre_actionset *msre_actionset_create_default(msre_engine *engine); + +void msre_actionset_init(msre_actionset *actionset, msre_rule *rule); + +#define FN_ACTION_VALIDATE(X) char *(*X)(msre_engine *engine, msre_action *action) +#define FN_ACTION_INIT(X) apr_status_t (*X)(msre_engine *engine, msre_actionset *actionset, msre_action *action) +#define FN_ACTION_EXECUTE(X) apr_status_t (*X)(modsec_rec *msr, apr_pool_t *mptmp, msre_rule *rule, msre_action *action) + +#define ACTION_DISRUPTIVE 1 +#define ACTION_NON_DISRUPTIVE 2 +#define ACTION_METADATA 3 +#define ACTION_FLOW 4 + +#define NO_PLUS_MINUS 0 +#define ALLOW_PLUS_MINUS 1 + +#define ACTION_CARDINALITY_ONE 1 +#define ACTION_CARDINALITY_MANY 2 + +struct msre_action_metadata { + const char *name; + unsigned int type; + unsigned int argc_min; + unsigned int argc_max; + unsigned int allow_param_plusminus; + unsigned int cardinality; + FN_ACTION_VALIDATE (validate); + FN_ACTION_INIT (init); + FN_ACTION_EXECUTE (execute); +}; + +struct msre_action { + msre_action_metadata *metadata; + const char *param; + const void *param_data; + unsigned int param_plusminus; /* ABSOLUTE_VALUE, POSITIVE_VALUE, NEGATIVE_VALUE */ +}; + +/* -- MSRE Function Prototypes ---------------------------------------------- */ + +msre_var_metadata *msre_resolve_var(msre_engine *engine, const char *name); + +int msre_parse_generic(apr_pool_t *pool, const char *text, apr_table_t *vartable, + char **error_msg); + +apr_status_t msre_parse_vars(msre_ruleset *ruleset, const char *text, + apr_array_header_t *arr, char **error_msg); + +char DSOLOCAL *msre_format_metadata(modsec_rec *msr, msre_actionset *actionset); + +#endif diff --git a/apache2/re_actions.c b/apache2/re_actions.c new file mode 100644 index 00000000..f6c8b230 --- /dev/null +++ b/apache2/re_actions.c @@ -0,0 +1,1796 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re_actions.c,v 1.9 2007/02/02 18:16:41 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "re.h" +#include + +/** + * Register action with the engine. + */ +static void msre_engine_action_register(msre_engine *engine, const char *name, unsigned int type, + unsigned int argc_min, unsigned int argc_max, unsigned int allow_param_plusminus, + unsigned int cardinality, FN_ACTION_VALIDATE(validate), FN_ACTION_INIT(init), + FN_ACTION_EXECUTE(execute)) +{ + msre_action_metadata *metadata = (msre_action_metadata *)apr_pcalloc(engine->mp, + sizeof(msre_action_metadata)); + if (metadata == NULL) return; + + metadata->name = name; + metadata->type = type; + metadata->argc_min = argc_min; + metadata->argc_max = argc_max; + metadata->allow_param_plusminus = allow_param_plusminus; + metadata->cardinality = cardinality; + metadata->validate = validate; + metadata->init = init; + metadata->execute = execute; + + apr_table_setn(engine->actions, name, (void *)metadata); +} + +/** + * Generates a single variable (from the supplied metadata). + */ +static msre_var *generate_single_var(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_pool_t *mptmp) +{ + apr_table_t *vartab = NULL; + const apr_table_entry_t *te = NULL; + const apr_array_header_t *arr = NULL; + + if (var->metadata->generate == NULL) return NULL; + vartab = apr_table_make(mptmp, 16); + var->metadata->generate(msr, var, rule, vartab, mptmp); + arr = apr_table_elts(vartab); + if (arr->nelts == 0) return NULL; + te = (apr_table_entry_t *)arr->elts; + + return (msre_var *)te[0].val; +} + +/** + * Expands macros ("%{NAME}" entities) if present + * in the given variable. + */ +int DSOLOCAL expand_macros(modsec_rec *msr, msc_string *var, msre_rule *rule, apr_pool_t *mptmp) { + char *data = NULL; + apr_array_header_t *arr = NULL; + char *p = NULL, *q = NULL, *t = NULL; + char *text_start = NULL, *next_text_start = NULL; + msc_string *part = NULL; + int i, offset = 0; + + if (var->value == NULL) return 0; + + /* IMP1 Duplicate the string and create the array on + * demand, thus not having to do it if there are + * no macros in the input data. + */ + + data = apr_pstrdup(mptmp, var->value); /* IMP1 Are we modifying data anywhere? */ + arr = apr_array_make(mptmp, 16, sizeof(msc_string *)); + if ((data == NULL)||(arr == NULL)) return -1; + + text_start = next_text_start = data; + do { + text_start = next_text_start; + p = strstr(text_start, "%"); + if (p != NULL) { + char *var_name = NULL; + char *var_value = NULL; + + if ((*(p + 1) == '{')&&(*(p + 2) != '\0')) { + char *var_start = p + 2; + + t = var_start; + while((*t != '\0')&&(*t != '}')) t++; + if (*t == '}') { + /* Named variable. */ + + var_name = apr_pstrmemdup(mptmp, var_start, t - var_start); + q = strstr(var_name, "."); + if (q != NULL) { + var_value = q + 1; + *q = '\0'; + } + + /* ENH Do we want to support %{DIGIT} as well? */ + + next_text_start = t + 1; /* *t was '}' */ + } else { + next_text_start = t; /* *t was '\0' */ + } + } else + if ((*(p + 1) >= '0')&&(*(p + 1) <= '9')) { + /* Special case for regex captures. */ + var_name = "TX"; + var_value = apr_pstrmemdup(mptmp, p + 1, 1); + next_text_start = p + 2; + } + + if (var_name != NULL) { + char *my_error_msg = NULL; + msre_var *var_generated = NULL; + msre_var *var_resolved = NULL; + + /* Add the text part before the macro to the array. */ + part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string)); + if (part == NULL) return -1; + part->value_len = p - text_start; + part->value = apr_pstrmemdup(mptmp, text_start, part->value_len); + *(msc_string **)apr_array_push(arr) = part; + + /* Resolve the macro and add that to the array. */ + var_resolved = msre_create_var_ex(msr->modsecurity->msre, var_name, var_value, + msr, &my_error_msg); + if (var_resolved != NULL) { + var_generated = generate_single_var(msr, var_resolved, rule, mptmp); + if (var_generated != NULL) { + part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string)); + if (part == NULL) return -1; + part->value_len = var_generated->value_len; + part->value = (char *)var_generated->value; + *(msc_string **)apr_array_push(arr) = part; + } + } else { + /* ENH Should we log something because the macro could not be resolved? */ + } + } else { + /* We could not identify a valid macro so add it as text. */ + part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string)); + if (part == NULL) return -1; + part->value_len = p - text_start; + part->value = apr_pstrmemdup(mptmp, text_start, part->value_len); + *(msc_string **)apr_array_push(arr) = part; + + next_text_start = p + 1; + } + } else { + /* Text part. */ + part = (msc_string *)apr_pcalloc(mptmp, sizeof(msc_string)); + part->value = apr_pstrdup(mptmp, text_start); + part->value_len = strlen(part->value); + *(msc_string **)apr_array_push(arr) = part; + } + } while (p != NULL); + + /* If there's more than one member of the array that + * means there was at least one macro present. Combine + * text parts into a single string now. + */ + if (arr->nelts > 1) { + /* Figure out the required size for the string. */ + var->value_len = 0; + for(i = 0; i < arr->nelts; i++) { + part = ((msc_string **)arr->elts)[i]; + var->value_len += part->value_len; + } + + /* Allocate the string. */ + var->value = apr_palloc(msr->mp, var->value_len + 1); + if (var->value == NULL) return -1; + + /* Combine the parts. */ + offset = 0; + for(i = 0; i < arr->nelts; i++) { + part = ((msc_string **)arr->elts)[i]; + memcpy((char *)(var->value + offset), part->value, part->value_len); + offset += part->value_len; + } + var->value[offset] = '\0'; + } + + return 1; +} + +/* id */ + +static apr_status_t msre_action_id_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->id = action->param; + return 1; +} + +/* rev */ + +static apr_status_t msre_action_rev_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->rev = action->param; + return 1; +} + +/* msg */ + +static apr_status_t msre_action_msg_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->msg = action->param; + return 1; +} + +/* severity */ + +static apr_status_t msre_action_severity_init(msre_engine *engine, + msre_actionset *actionset, msre_action *action) +{ + actionset->severity = atoi(action->param); + return 1; +} + +/* chain */ + +static apr_status_t msre_action_chain_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->is_chained = 1; + return 1; +} + +/* log */ +static apr_status_t msre_action_log_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->log = 1; + return 1; +} + +/* nolog */ +static apr_status_t msre_action_nolog_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->log = 0; + actionset->auditlog = 0; + return 1; +} + +/* auditlog */ +static apr_status_t msre_action_auditlog_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->auditlog = 1; + return 1; +} + +/* noauditlog */ +static apr_status_t msre_action_noauditlog_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->auditlog = 0; + return 1; +} + +/* deny */ +static apr_status_t msre_action_deny_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_DENY; + return 1; +} + +/* status */ +static char *msre_action_status_validate(msre_engine *engine, msre_action *action) { + /* ENH action->param must be a valid HTTP status code. */ + return NULL; +} + +static apr_status_t msre_action_status_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_status = atoi(action->param); + return 1; +} + +/* drop */ +static apr_status_t msre_action_drop_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_DROP; + return 1; +} + +/* pause */ +static char *msre_action_pause_validate(msre_engine *engine, msre_action *action) { + /* ENH Validate a positive number. */ + return NULL; +} + +static apr_status_t msre_action_pause_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_pause = atoi(action->param); + return 1; +} + +/* redirect */ + +static char *msre_action_redirect_validate(msre_engine *engine, msre_action *action) { + /* ENH Add validation. */ + return NULL; +} + +static apr_status_t msre_action_redirect_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_REDIRECT; + actionset->intercept_uri = action->param; + return 1; +} + +static apr_status_t msre_action_redirect_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + msc_string *var = NULL; + + var = apr_pcalloc(mptmp, sizeof(msc_string)); + if (var == NULL) return -1; + var->value = (char *)action->param; + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + + rule->actionset->intercept_uri = apr_pstrmemdup(msr->mp, var->value, var->value_len); + + return 1; +} + +/* proxy */ + +static char *msre_action_proxy_validate(msre_engine *engine, msre_action *action) { + /* ENH Add validation. */ + return NULL; +} + +static apr_status_t msre_action_proxy_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_PROXY; + actionset->intercept_uri = action->param; + return 1; +} + +static apr_status_t msre_action_proxy_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + msc_string *var = NULL; + + var = apr_pcalloc(mptmp, sizeof(msc_string)); + if (var == NULL) return -1; + var->value = (char *)action->param; + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + + rule->actionset->intercept_uri = apr_pstrmemdup(msr->mp, var->value, var->value_len); + + return 1; +} + +/* pass */ + +static apr_status_t msre_action_pass_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_NONE; + return 1; +} + +/* skip */ + +static char *msre_action_skip_validate(msre_engine *engine, msre_action *action) { + /* ENH Add validation. */ + return NULL; +} + +static apr_status_t msre_action_skip_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->skip_count = atoi(action->param); + if (actionset->skip_count <= 0) actionset->skip_count = 1; + return 1; +} + +/* allow */ + +static apr_status_t msre_action_allow_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->intercept_action = ACTION_ALLOW; + return 1; +} + +/* phase */ + +static char *msre_action_phase_validate(msre_engine *engine, msre_action *action) { + /* ENH Add validation. */ + return NULL; +} + +static apr_status_t msre_action_phase_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + actionset->phase = atoi(action->param); + return 1; +} + +/* t */ + +static char *msre_action_t_validate(msre_engine *engine, msre_action *action) { + msre_tfn_metadata *metadata = NULL; + metadata = msre_engine_tfn_resolve(engine, action->param); + if (metadata == NULL) return apr_psprintf(engine->mp, "Invalid transformation function: %s", + action->param); + action->param_data = metadata; + return NULL; +} + +static apr_status_t msre_action_t_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + msre_tfn_metadata *metadata = (msre_tfn_metadata *)action->param_data; + action->param_data = metadata; + return 1; +} + +/* ctl */ +static char *msre_action_ctl_validate(msre_engine *engine, msre_action *action) { + char *name = NULL; + char *value = NULL; + + /* Parse first. */ + if (parse_name_eq_value(engine->mp, action->param, &name, &value) < 0) { + return FATAL_ERROR; + } + if (value == NULL) { + return apr_psprintf(engine->mp, "Missing ctl value for name: %s", name); + } + + /* Validate value. */ + if (strcmp(name, "ruleEngine") == 0) { + if (strcasecmp(value, "on") == 0) return NULL; + if (strcasecmp(value, "off") == 0) return NULL; + if (strcasecmp(value, "detectiononly") == 0) return NULL; + return apr_psprintf(engine->mp, "Invalid setting for ctl name ruleEngine: %s", value); + } else + if (strcmp(name, "requestBodyAccess") == 0) { + if (parse_boolean(value) == -1) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + " requestBodyAccess: %s", value); + } + return NULL; + } else + if (strcmp(name, "requestBodyProcessor") == 0) { + /* ENH We will accept anything for now but it'd be nice + * to add a check here that the processor name is a valid one. + */ + return NULL; + } else + if (strcmp(name, "responseBodyAccess") == 0) { + if (parse_boolean(value) == -1) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + " responseBodyAccess: %s", value); + } + return NULL; + } else + if (strcmp(name, "auditEngine") == 0) { + if (strcasecmp(value, "on") == 0) return NULL; + if (strcasecmp(value, "off") == 0) return NULL; + if (strcasecmp(value, "relevantonly") == 0) return NULL; + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + " auditEngine: %s", value); + } else + if (strcmp(name, "auditLogParts") == 0) { + if ((value[0] == '+')||(value[0] == '-')) { + if (is_valid_parts_specification(value + 1) != 1) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + "auditLogParts: %s", value); + } + } + else + if (is_valid_parts_specification(value) != 1) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + "auditLogParts: %s", value); + } + return NULL; + } else + if (strcmp(name, "debugLogLevel") == 0) { + if ((atoi(value) >= 0)&&(atoi(value) <= 9)) return NULL; + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + "debugLogLevel: %s", value); + } else + if (strcmp(name, "requestBodyLimit") == 0) { + long int limit = strtol(value, NULL, 10); + + if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + "requestBodyLimit: %s", value); + } + + if (limit > REQUEST_BODY_HARD_LIMIT) { + return apr_psprintf(engine->mp, "Request size limit cannot exceed " + "the hard limit: %li", RESPONSE_BODY_HARD_LIMIT); + } + + return NULL; + } else + if (strcmp(name, "responseBodyLimit") == 0) { + long int limit = strtol(value, NULL, 10); + + if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) { + return apr_psprintf(engine->mp, "Invalid setting for ctl name " + "responseBodyLimit: %s", value); + } + + if (limit > RESPONSE_BODY_HARD_LIMIT) { + return apr_psprintf(engine->mp, "Response size limit cannot exceed " + "the hard limit: %li", RESPONSE_BODY_HARD_LIMIT); + } + + return NULL; + } + else { + return apr_psprintf(engine->mp, "Invalid ctl name setting: %s", name); + } +} + +static apr_status_t msre_action_ctl_init(msre_engine *engine, msre_actionset *actionset, + msre_action *action) +{ + /* Do nothing. */ + return 1; +} + +static apr_status_t msre_action_ctl_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *name = NULL; + char *value = NULL; + + /* Parse first. */ + if (parse_name_eq_value(msr->mp, action->param, &name, &value) < 0) return -1; + if (value == NULL) return -1; + + /* Validate value. */ + if (strcmp(name, "ruleEngine") == 0) { + if (strcasecmp(value, "on") == 0) { + msr->txcfg->is_enabled = MODSEC_ENABLED; + msr->usercfg->is_enabled = MODSEC_ENABLED; + } + + if (strcasecmp(value, "off") == 0) { + msr->txcfg->is_enabled = MODSEC_DISABLED; + msr->usercfg->is_enabled = MODSEC_DISABLED; + } + + if (strcasecmp(value, "detectiononly") == 0) { + msr->txcfg->is_enabled = MODSEC_DETECTION_ONLY; + msr->usercfg->is_enabled = MODSEC_DETECTION_ONLY; + } + + return 1; + } else + if (strcmp(name, "requestBodyAccess") == 0) { + int pv = parse_boolean(value); + + if (pv == -1) return -1; + msr->txcfg->reqbody_access = pv; + msr->usercfg->reqbody_access = pv; + msr_log(msr, 4, "Ctl: Set requestBodyAccess to %i.", pv); + + return 1; + } else + if (strcmp(name, "requestBodyProcessor") == 0) { + msr->msc_reqbody_processor = value; + msr_log(msr, 4, "Ctl: Set requestBodyProcessor to %s.", value); + + return 1; + } else + if (strcmp(name, "responseBodyAccess") == 0) { + int pv = parse_boolean(value); + + if (pv == -1) return -1; + msr->txcfg->resbody_access = pv; + msr->usercfg->resbody_access = pv; + msr_log(msr, 4, "Ctl: Set responseBodyAccess to %i.", pv); + + return 1; + } else + if (strcmp(name, "auditEngine") == 0) { + if (strcasecmp(value, "on") == 0) { + msr->txcfg->auditlog_flag = AUDITLOG_ON; + msr->usercfg->auditlog_flag = AUDITLOG_ON; + } + + if (strcasecmp(value, "off") == 0) { + msr->txcfg->auditlog_flag = AUDITLOG_OFF; + msr->usercfg->auditlog_flag = AUDITLOG_OFF; + } + + if (strcasecmp(value, "relevantonly") == 0) { + msr->txcfg->auditlog_flag = AUDITLOG_RELEVANT; + msr->usercfg->auditlog_flag = AUDITLOG_RELEVANT; + } + + msr_log(msr, 4, "Ctl: Set auditEngine to %i.", msr->txcfg->auditlog_flag); // TODO + + return 1; + } else + if (strcmp(name, "auditLogParts") == 0) { + char *new_value = value; + + if (value[0] == '+') { + /* Add the listed parts. */ + new_value = apr_pstrcat(msr->mp, msr->txcfg->auditlog_parts, value + 1, NULL); + } + else + if (value[0] == '-') { /* Remove the listed parts. */ + char c, *t = value + 1; + + /* Start with the current value. */ + new_value = apr_pstrdup(msr->mp, msr->txcfg->auditlog_parts); + + while((c = *t++) != '\0') { + char *s = new_value; + char *d = new_value; + + while(*s != '\0') { + if (*s != c) { + *d++ = *s++; + } else { + *s++; + } + } + *d = '\0'; + } + } + + /* Set the new value. */ + msr->txcfg->auditlog_parts = new_value; + msr->usercfg->auditlog_parts = new_value; + msr_log(msr, 4, "Ctl: Set auditLogParts to %s.", msr->txcfg->auditlog_parts); + + return 1; + } else + if (strcmp(name, "debugLogLevel") == 0) { + msr->txcfg->debuglog_level = atoi(value); + msr->usercfg->debuglog_level = atoi(value); + msr_log(msr, 4, "Ctl: Set debugLogLevel to %i.", msr->txcfg->debuglog_level); + + return 1; + } else + if (strcmp(name, "requestBodyLimit") == 0) { + long int limit = strtol(value, NULL, 10); + + /* ENH Accept only in correct phase warn otherwise. */ + msr->txcfg->reqbody_limit = limit; + msr->usercfg->reqbody_limit = limit; + + return 1; + } else + if (strcmp(name, "responseBodyLimit") == 0) { + long int limit = strtol(value, NULL, 10); + + /* ENH Accept only in correct phase warn otherwise. */ + msr->txcfg->of_limit = limit; + msr->usercfg->of_limit = limit; + + return 1; + } + else { + /* ENH Should never happen, but log if it does. */ + return -1; + } +} + +/* xmlns */ +static char *msre_action_xmlns_validate(msre_engine *engine, msre_action *action) { + char *name = NULL; + char *value = NULL; + + /* Parse first. */ + if (parse_name_eq_value(engine->mp, action->param, &name, &value) < 0) { + return FATAL_ERROR; + } + if (value == NULL) { + return apr_psprintf(engine->mp, "Missing xmlns href for prefix: %s", name); + } + + /* Don't do anything else right now, we are just storing + * the value for the variable, which is the real consumer + * for the namespace information. + */ + + return NULL; +} + +/* sanitiseArg */ +static apr_status_t msre_action_sanitiseArg_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + const char *sargname = NULL; + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + + sargname = action->param; + + tarr = apr_table_elts(msr->arguments); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msc_arg *arg = (msc_arg *)telts[i].val; + + if (strcasecmp(sargname, arg->name) == 0) { + apr_table_addn(msr->arguments_to_sanitise, arg->name, (void *)arg); + } + } + + return 1; +} + +#define SANITISE_ARG 1 +#define SANITISE_REQUEST_HEADER 2 +#define SANITISE_RESPONSE_HEADER 3 + +/* sanitiseMatched */ +static apr_status_t msre_action_sanitiseMatched_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + const char *sargname = NULL; + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i, type = 0; + + if (msr->matched_var == NULL) return 0; + + /* IMP1 We need to extract the variable name properly here, + * taking into account it may have been escaped. + */ + if (strncmp(msr->matched_var, "ARGS:", 5) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 5); + type = SANITISE_ARG; + } else + if (strncmp(msr->matched_var, "ARGS_NAMES:", 11) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 11); + type = SANITISE_ARG; + } else + if (strncmp(msr->matched_var, "REQUEST_HEADERS:", 16) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 16); + type = SANITISE_REQUEST_HEADER; + } else + if (strncmp(msr->matched_var, "REQUEST_HEADERS_NAMES:", 22) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 22); + type = SANITISE_REQUEST_HEADER; + } else + if (strncmp(msr->matched_var, "RESPONSE_HEADERS:", 17) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 17); + type = SANITISE_RESPONSE_HEADER; + } else + if (strncmp(msr->matched_var, "RESPONSE_HEADERS_NAMES:", 23) == 0) { + sargname = apr_pstrdup(msr->mp, msr->matched_var + 23); + type = SANITISE_RESPONSE_HEADER; + } + else { + msr_log(msr, 3, "sanitiseMatched: Don't know how to handle variable: %s", + msr->matched_var); + return 0; + } + + switch(type) { + case SANITISE_ARG : + tarr = apr_table_elts(msr->arguments); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msc_arg *arg = (msc_arg *)telts[i].val; + if (strcasecmp(sargname, arg->name) == 0) { + apr_table_addn(msr->arguments_to_sanitise, arg->name, (void *)arg); + } + } + break; + + case SANITISE_REQUEST_HEADER : + apr_table_set(msr->request_headers_to_sanitise, sargname, "1"); + break; + + case SANITISE_RESPONSE_HEADER : + apr_table_set(msr->response_headers_to_sanitise, sargname, "1"); + break; + + default : + /* do nothing */ + break; + } + + return 1; +} + +/* sanitiseRequestHeader */ +static apr_status_t msre_action_sanitiseRequestHeader_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + apr_table_set(msr->request_headers_to_sanitise, action->param, "1"); + return 1; +} + +/* sanitiseResponseHeader */ +static apr_status_t msre_action_sanitiseResponseHeader_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + apr_table_set(msr->response_headers_to_sanitise, action->param, "1"); + return 1; +} + +/* setenv */ +static apr_status_t msre_action_setenv_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *data = apr_pstrdup(mptmp, action->param); + char *env_name = NULL, *env_value = NULL; + char *s = NULL; + + /* Extract the name and the value. */ + /* IMP1 We have a function for this now, parse_name_eq_value? */ + s = strstr(data, "="); + if (s == NULL) { + env_name = data; + env_value = "1"; + } else { + env_name = data; + env_value = s + 1; + *s = '\0'; + } + + /* Execute the requested action. */ + if (env_name[0] == '!') { + /* Delete */ + apr_table_unset(msr->r->subprocess_env, env_name + 1); + } else { + /* Set */ + apr_table_set(msr->r->subprocess_env, env_name, env_value); + } + + return 1; +} + +/* setvar */ +static apr_status_t msre_action_setvar_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *data = apr_pstrdup(mptmp, action->param); + char *col_name = NULL, *var_name = NULL, *var_value = NULL; + char *s = NULL; + apr_table_t *target_col = NULL; + int is_negated = 0; + + /* Extract the name and the value. */ + /* IMP1 We have a function for this now, parse_name_eq_value? */ + s = strstr(data, "="); + if (s == NULL) { + var_name = data; + var_value = "1"; + } else { + var_name = data; + var_value = s + 1; + *s = '\0'; + + while ((*var_value != '\0')&&(isspace(*var_value))) var_value++; + } + + /* Handle the exclamation mark. */ + if (var_name[0] == '!') { + var_name = var_name + 1; + is_negated = 1; + } + + /* ENH Not possible to use ! and = at the same time. */ + /* ENH Not possible to change variable "KEY". */ + + /* Figure out the collection name. */ + target_col = msr->tx_vars; + s = strstr(var_name, "."); + if (s == NULL) { + /* ENH Log warning detected variable name but no collection. */ + return 0; + } + col_name = var_name; + var_name = s + 1; + *s = '\0'; + + /* Locate the collection. */ + if (strcasecmp(col_name, "tx") == 0) { /* Special case for TX variables. */ + target_col = msr->tx_vars; + } else { + target_col = (apr_table_t *)apr_table_get(msr->collections, col_name); + if (target_col == NULL) { + msr_log(msr, 3, "Could not set variable \"%s.%s\" as the collection does not exist.", + log_escape(msr->mp, col_name), log_escape(msr->mp, var_name)); + return 0; + } + } + + if (is_negated) { + /* Unset variable. */ + + /* ENH Refuse to remove certain variables, e.g. TIMEOUT, internal variables, etc... */ + + apr_table_unset(target_col, var_name); + msr_log(msr, 9, "Unset variable \"%s.%s\".", log_escape(mptmp, col_name), + log_escape(mptmp, var_name)); + } else { + /* Set or change variable. */ + + if ((var_value[0] == '+')||(var_value[0] == '-')) { + /* Relative change. */ + msc_string *var = NULL; + int value = 0; + + /* Retrieve variable or generate (if it does not exist). */ + var = (msc_string *)apr_table_get(target_col, var_name); + if (var == NULL) { + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = apr_pstrdup(msr->mp, var_name); + var->name_len = strlen(var->name); + value = 0; + } else { + value = atoi(var->value); + } + + /* Change value. */ + value += atoi(var_value); + if (value < 0) value = 0; /* Counters never go below zero. */ + + /* Put the variable back. */ + var->value = apr_psprintf(msr->mp, "%i", value); + var->value_len = strlen(var->value); + apr_table_setn(target_col, var->name, (void *)var); + + msr_log(msr, 9, "Set variable \"%s.%s\" to \"%s\".", log_escape(mptmp, col_name), + log_escape(mptmp, var->name), log_escape(mptmp, var->value)); + } else { + /* Absolute change. */ + + msc_string *var = NULL; + + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = apr_pstrdup(msr->mp, var_name); + var->name_len = strlen(var->name); + var->value = apr_pstrdup(msr->mp, var_value); + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + apr_table_setn(target_col, var->name, (void *)var); + + msr_log(msr, 9, "Set variable \"%s.%s\" to \"%s\".", log_escape(mptmp, col_name), + log_escape(mptmp, var->name), log_escape(mptmp, var->value)); + } + } + + /* Make note of the change so that we know later + * we need to persist the collection. + */ + apr_table_set(msr->collections_dirty, col_name, "1"); + + return 1; +} + +/* expirevar */ +static apr_status_t msre_action_expirevar_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *data = apr_pstrdup(mptmp, action->param); + char *col_name = NULL, *var_name = NULL, *var_value = NULL; + char *s = NULL; + apr_table_t *target_col = NULL; + msc_string *var = NULL; + + /* Extract the name and the value. */ + /* IMP1 We have a function for this now, parse_name_eq_value? */ + s = strstr(data, "="); + if (s == NULL) { + var_name = data; + var_value = "1"; + } else { + var_name = data; + var_value = s + 1; + *s = '\0'; + } + + /* Choose the collection to work with. */ + s = strstr(var_name, "."); + if (s != NULL) { + col_name = var_name; + var_name = s + 1; + *s = '\0'; + + /* IMP1 No need to handle TX here because TX variables cannot expire, + * but we definitely need to have a better error message. + */ + + target_col = (apr_table_t *)apr_table_get(msr->collections, col_name); + if (target_col == NULL) { + msr_log(msr, 3, "Could not set variable \"%s.%s\" as the collection does not exist.", + log_escape(msr->mp, col_name), log_escape(msr->mp, var_name)); + return 0; + } + } else { + /* ENH Log warning detected variable name but no collection. */ + return 0; + } + + /* To expire a variable we just place a special variable into + * the collection. Expiry actually happens when the collection + * is retrieved from storage the next time. + */ + var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = apr_psprintf(msr->mp, "__expire_%s", var_name); + var->name_len = strlen(var->name); + var->value = apr_psprintf(msr->mp, "%i", (int)(apr_time_sec(msr->request_time) + + atoi(var_value))); + var->value_len = strlen(var->value); + apr_table_setn(target_col, var->name, (void *)var); + + msr_log(msr, 4, "Variable \"%s.%s\" set to expire in %s seconds.", col_name, + var_name, var_value); + + apr_table_set(msr->collections_dirty, col_name, "1"); + + return 1; +} + +/* deprecatevar */ +static apr_status_t msre_action_deprecatevar_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *data = apr_pstrdup(mptmp, action->param); + char *col_name = NULL, *var_name = NULL, *var_value = NULL; + char *s = NULL; + apr_table_t *target_col = NULL; + msc_string *var = NULL, *var_last_update_time = NULL; + unsigned int last_update_time, current_time; + long int current_value, new_value; + + /* Extract the name and the value. */ + /* IMP1 We have a function for this now, parse_name_eq_value? */ + s = strstr(data, "="); + if (s == NULL) { + var_name = data; + var_value = "1"; + } else { + var_name = data; + var_value = s + 1; + *s = '\0'; + } + + /* Choose the collection to work with. */ + s = strstr(var_name, "."); + if (s != NULL) { + col_name = var_name; + var_name = s + 1; + *s = '\0'; + + /* IMP1 Add message TX variables cannot deprecate in value. */ + + target_col = (apr_table_t *)apr_table_get(msr->collections, col_name); + if (target_col == NULL) { + msr_log(msr, 3, "Could not deprecate variable \"%s.%s\" as the collection does " + "not exist.", log_escape(msr->mp, col_name), log_escape(msr->mp, var_name)); + return 0; + } + } else { + /* ENH Log warning detected variable name but no collection. */ + return 0; + } + + /* Find the current value. */ + var = (msc_string *)apr_table_get(target_col, var_name); + if (var == NULL) { + msr_log(msr, 9, "Asked to deprecate variable \"%s.%s\" but it does not exist.", + log_escape(msr->mp, col_name), log_escape(msr->mp, var_name)); + return 0; + } + current_value = atoi(var->value); + + /* Find the last update time (of the collection). */ + var_last_update_time = (msc_string *)apr_table_get(target_col, "LAST_UPDATE_TIME"); + if (var_last_update_time == NULL) { + msr_log(msr, 1, "Internal Error: Collection missing LAST_UPDATE_TIME."); + return 0; + } + + current_time = (unsigned int)apr_time_sec(apr_time_now()); + last_update_time = atoi(var_last_update_time->value); + + s = strstr(var_value, "/"); + if (s == NULL) { + msr_log(msr, 3, "Incorrect format for the deprecatevar argument: \"%s\"", + log_escape(msr->mp, var_value)); + return 0; + } + *s = '\0'; + s++; + + /* Deprecate the value using the given speed and the + * time elapsed since the last update. + */ + new_value = current_value - + ((current_time - last_update_time) * atoi(var_value) / atoi(s)); + if (new_value < 0) new_value = 0; + + /* Only change the value if it differs. */ + if (new_value != current_value) { + var->value = apr_psprintf(msr->mp, "%i", (int)new_value); + var->value_len = strlen(var->value); + + msr_log(msr, 4, "Deprecated variable \"%s.%s\" from %li to %li (%i seconds since " + "last update).", log_escape(msr->mp, col_name), log_escape(msr->mp, var_name), + current_value, new_value, current_time - last_update_time); + + apr_table_set(msr->collections_dirty, col_name, "1"); + } else { + msr_log(msr, 9, "Not deprecating variable \"%s.%s\" because the new value (%li) is " + "the same as the old one (%li) (%i seconds since last update).", + log_escape(msr->mp, col_name), log_escape(msr->mp, var_name), current_value, + new_value, current_time - last_update_time); + } + + return 1; +} + +static apr_status_t init_collection(modsec_rec *msr, const char *real_col_name, + const char *col_name, const char *col_key, unsigned int col_key_len) +{ + apr_table_t *table = NULL; + + /* IMP1 Cannot initialise the built-in collections this way. */ + + /* Does the collection exist already? */ + if (apr_table_get(msr->collections, col_name) != NULL) { + /* ENH Warn about this. */ + return 0; + } + + /* Init collection from storage. */ + table = collection_retrieve(msr, real_col_name, col_key, col_key_len); + + if (table == NULL) { + msc_string *var = NULL; + + /* Does not exist yet - create new. */ + msr_log(msr, 4, "Creating collection (name \"%s\", key \"%s\").", + real_col_name, col_key); + + table = apr_table_make(msr->mp, 24); + + /* IMP1 Is the timeout hard-coded to 3600? */ + + /* Add default timeout. */ + var = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "__expire_KEY"; + var->name_len = strlen(var->name); + var->value = apr_psprintf(msr->mp, "%i", (int)(apr_time_sec(msr->request_time) + 3600)); + var->value_len = strlen(var->value); + apr_table_setn(table, var->name, (void *)var); + + /* Remember the key. */ + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "KEY"; + var->name_len = strlen(var->name); + var->value = apr_pstrmemdup(msr->mp, col_key, col_key_len); + var->value_len = col_key_len; + apr_table_setn(table, var->name, (void *)var); + + /* The timeout. */ + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "TIMEOUT"; + var->name_len = strlen(var->name); + var->value = apr_psprintf(msr->mp, "%i", 3600); + var->value_len = strlen(var->value); + apr_table_setn(table, var->name, (void *)var); + + /* We may want to allow the user to unset KEY + * but we still need to preserve value to identify + * the collection in storage. + */ + + /* IMP1 Actually I want a better way to delete collections, + * perhaps a dedicated action. + */ + + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "__key"; + var->name_len = strlen(var->name); + var->value = apr_pstrmemdup(msr->mp, col_key, col_key_len); + var->value_len = col_key_len; + apr_table_setn(table, var->name, (void *)var); + + /* Peristence code will need to know the name of the collection. */ + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "__name"; + var->name_len = strlen(var->name); + var->value = apr_pstrdup(msr->mp, real_col_name); + var->value_len = strlen(var->value); + apr_table_setn(table, var->name, (void *)var); + + /* Create time. */ + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "CREATE_TIME"; + var->name_len = strlen(var->name); + var->value = apr_psprintf(msr->mp, "%i", (int)apr_time_sec(msr->request_time)); + var->value_len = strlen(var->value); + apr_table_setn(table, var->name, (void *)var); + + /* Update counter. */ + var = apr_pcalloc(msr->mp, sizeof(msc_string)); + var->name = "UPDATE_COUNTER"; + var->name_len = strlen(var->name); + var->value = "0"; + var->value_len = strlen(var->value); + apr_table_setn(table, var->name, (void *)var); + } + + /* Add the collection to the list. */ + apr_table_setn(msr->collections, apr_pstrdup(msr->mp, col_name), (void *)table); + + if (strcmp(col_name, real_col_name) != 0) { + msr_log(msr, 4, "Added collection \"%s\" to the list as \"%s\".", + log_escape(msr->mp, real_col_name), log_escape(msr->mp, col_name)); + } else { + msr_log(msr, 4, "Added collection \"%s\" to the list.", + log_escape(msr->mp, real_col_name)); + } + + return 1; +} + +/* initcol */ +static apr_status_t msre_action_initcol_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *data = apr_pstrdup(msr->mp, action->param); + char *col_name = NULL, *col_key = NULL; + unsigned int col_key_len; + + msc_string *var = NULL; + char *s = NULL; + + /* Extract the name and the value. */ + /* IMP1 We have a function for this now, parse_name_eq_value? */ + s = strstr(data, "="); + if (s == NULL) return 0; + col_name = data; + col_key = s + 1; + *s = '\0'; + + /* Expand the key and init collection from storage. */ + var = apr_pcalloc(mptmp, sizeof(msc_string)); + var->value = col_key; + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + + col_key = var->value; + col_key_len = var->value_len; + + return init_collection(msr, col_name, col_name, col_key, col_key_len); +} + +/* setsid */ +static apr_status_t msre_action_setsid_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + msc_string *var = NULL; + char *real_col_name = NULL, *col_key = NULL; + unsigned int col_key_len; + + /* Construct session ID. */ + var = apr_pcalloc(mptmp, sizeof(msc_string)); + var->value = (char *)action->param; + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + msr->sessionid = apr_pstrdup(msr->mp, var->value); + + /* Construct collection name. */ + col_key = var->value; + col_key_len = var->value_len; + real_col_name = apr_psprintf(mptmp, "%s_SESSION", msr->txcfg->webappid); + + /* Initialise collection. */ + return init_collection(msr, real_col_name, "SESSION", col_key, col_key_len); +} + +/* setuid */ +static apr_status_t msre_action_setuid_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + msc_string *var = NULL; + char *real_col_name = NULL, *col_key = NULL; + unsigned int col_key_len; + + /* Construct user ID. */ + var = apr_pcalloc(mptmp, sizeof(msc_string)); + var->value = (char *)action->param; + var->value_len = strlen(var->value); + expand_macros(msr, var, rule, mptmp); + msr->userid = apr_pstrdup(msr->mp, var->value); + + /* Construct collection name. */ + col_key = var->value; + col_key_len = var->value_len; + real_col_name = apr_psprintf(mptmp, "%s_USER", msr->txcfg->webappid); + + /* Initialise collection. */ + return init_collection(msr, real_col_name, "USER", col_key, col_key_len); +} + +/* exec */ +static apr_status_t msre_action_exec_execute(modsec_rec *msr, apr_pool_t *mptmp, + msre_rule *rule, msre_action *action) +{ + char *script_output = NULL; + + int rc = apache2_exec(msr, action->param, NULL, &script_output); + if (rc != 1) { + msr_log(msr, 1, "Failed to execute: %s", action->param); + return 0; + } + + return 1; +} + +/* -- */ + +/** + * + */ +void msre_engine_register_default_actions(msre_engine *engine) { + + /* id */ + msre_engine_action_register(engine, + "id", + ACTION_METADATA, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_id_init, + NULL + ); + + /* rev */ + msre_engine_action_register(engine, + "rev", + ACTION_METADATA, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_rev_init, + NULL + ); + + /* msg */ + msre_engine_action_register(engine, + "msg", + ACTION_METADATA, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_msg_init, + NULL + ); + + /* severity */ + msre_engine_action_register(engine, + "severity", + ACTION_METADATA, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_severity_init, + NULL + ); + + /* chain */ + msre_engine_action_register(engine, + "chain", + ACTION_FLOW, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_chain_init, + NULL + ); + + /* log */ + msre_engine_action_register(engine, + "log", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_log_init, + NULL + ); + + /* nolog */ + msre_engine_action_register(engine, + "nolog", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_nolog_init, + NULL + ); + + /* auditlog */ + msre_engine_action_register(engine, + "auditlog", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_auditlog_init, + NULL + ); + + /* noauditlog */ + msre_engine_action_register(engine, + "noauditlog", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_noauditlog_init, + NULL + ); + + /* deny */ + msre_engine_action_register(engine, + "deny", + ACTION_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_deny_init, + NULL + ); + + /* status */ + msre_engine_action_register(engine, + "status", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_status_validate, + msre_action_status_init, + NULL + ); + + /* drop */ + msre_engine_action_register(engine, + "drop", + ACTION_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_drop_init, + NULL + ); + + /* pause */ + msre_engine_action_register(engine, + "pause", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_pause_validate, + msre_action_pause_init, + NULL + ); + + /* redirect */ + msre_engine_action_register(engine, + "redirect", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_redirect_validate, + msre_action_redirect_init, + msre_action_redirect_execute + ); + + /* proxy */ + msre_engine_action_register(engine, + "proxy", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_proxy_validate, + msre_action_proxy_init, + msre_action_proxy_execute + ); + + /* pass */ + msre_engine_action_register(engine, + "pass", + ACTION_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_pass_init, + NULL + ); + + /* skip */ + msre_engine_action_register(engine, + "skip", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_skip_validate, + msre_action_skip_init, + NULL + ); + + /* allow */ + msre_engine_action_register(engine, + "allow", + ACTION_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + msre_action_allow_init, + NULL + ); + + /* phase */ + msre_engine_action_register(engine, + "phase", + ACTION_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + msre_action_phase_validate, + msre_action_phase_init, + NULL + ); + + /* t */ + msre_engine_action_register(engine, + "t", + ACTION_NON_DISRUPTIVE, + 1, 1, + ALLOW_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + msre_action_t_validate, + msre_action_t_init, + NULL + ); + + /* ctl */ + msre_engine_action_register(engine, + "ctl", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + msre_action_ctl_validate, + msre_action_ctl_init, + msre_action_ctl_execute + ); + + /* xmlns */ + msre_engine_action_register(engine, + "xmlns", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + msre_action_xmlns_validate, + NULL, + NULL + ); + + /* capture */ + msre_engine_action_register(engine, + "capture", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + NULL, + NULL + ); + + /* sanitiseArg */ + msre_engine_action_register(engine, + "sanitiseArg", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_sanitiseArg_execute + ); + + /* sanitiseMatched */ + msre_engine_action_register(engine, + "sanitiseMatched", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_sanitiseMatched_execute + ); + + /* sanitiseRequestHeader */ + msre_engine_action_register(engine, + "sanitiseRequestHeader", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_sanitiseRequestHeader_execute + ); + + /* sanitiseResponseHeader */ + msre_engine_action_register(engine, + "sanitiseResponseHeader", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_sanitiseResponseHeader_execute + ); + + /* setenv */ + msre_engine_action_register(engine, + "setenv", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_setenv_execute + ); + + /* setvar */ + msre_engine_action_register(engine, + "setvar", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_setvar_execute + ); + + /* expirevar */ + msre_engine_action_register(engine, + "expirevar", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_expirevar_execute + ); + + /* deprecatevar */ + msre_engine_action_register(engine, + "deprecatevar", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_deprecatevar_execute + ); + + /* initcol */ + msre_engine_action_register(engine, + "initcol", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_initcol_execute + ); + + /* setsid */ + msre_engine_action_register(engine, + "setsid", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + NULL, + msre_action_setsid_execute + ); + + /* setuid */ + msre_engine_action_register(engine, + "setuid", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + NULL, + msre_action_setuid_execute + ); + + /* exec */ + msre_engine_action_register(engine, + "exec", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + msre_action_exec_execute + ); + + /* multiMatch */ + msre_engine_action_register(engine, + "multiMatch", + ACTION_NON_DISRUPTIVE, + 0, 0, + NO_PLUS_MINUS, + ACTION_CARDINALITY_ONE, + NULL, + NULL, + NULL + ); + + /* tag */ + msre_engine_action_register(engine, + "tag", + ACTION_NON_DISRUPTIVE, + 1, 1, + NO_PLUS_MINUS, + ACTION_CARDINALITY_MANY, + NULL, + NULL, + NULL + ); +} diff --git a/apache2/re_operators.c b/apache2/re_operators.c new file mode 100644 index 00000000..80284c4a --- /dev/null +++ b/apache2/re_operators.c @@ -0,0 +1,986 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re_operators.c,v 1.7 2007/01/23 16:08:15 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "re.h" +#include "msc_pcre.h" +#include "apr_strmatch.h" + +/** + * + */ +void msre_engine_op_register(msre_engine *engine, const char *name, + FN_OP_PARAM_INIT(fn1), FN_OP_EXECUTE(fn2)) +{ + msre_op_metadata *metadata = (msre_op_metadata *)apr_pcalloc(engine->mp, + sizeof(msre_op_metadata)); + if (metadata == NULL) return; + + metadata->name = name; + metadata->param_init = fn1; + metadata->execute = fn2; + apr_table_setn(engine->operators, name, (void *)metadata); +} + +/** + * + */ +msre_op_metadata *msre_engine_op_resolve(msre_engine *engine, const char *name) { + return (msre_op_metadata *)apr_table_get(engine->operators, name); +} + + + +/* -- Operators -- */ + +/* unconditionalMatch */ + +static int msre_op_unconditionalmatch_execute(modsec_rec *msr, msre_rule *rule, + msre_var *var, char **error_msg) +{ + *error_msg = "Unconditional match in SecAction."; + + /* Always match. */ + return 1; +} + +/* rx */ + +static int msre_op_rx_param_init(msre_rule *rule, char **error_msg) { + const char *errptr = NULL; + int erroffset; + msc_regex_t *regex; + const char *pattern = rule->op_param; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* Compile pattern */ + regex = msc_pregcomp(rule->ruleset->mp, pattern, 0, &errptr, &erroffset); + if (regex == NULL) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Error compiling pattern (pos %i): %s", + erroffset, errptr); + return 0; + } + + rule->op_param_data = regex; + + return 1; /* OK */ +} + +static int msre_op_rx_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) { + msc_regex_t *regex = (msc_regex_t *)rule->op_param_data; + const char *target; + unsigned int target_length; + char *my_error_msg = NULL; + int ovector[33]; + int rc; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (regex == NULL) { + *error_msg = "Internal Error: regex data is null."; + return -1; + } + + /* If the given target is null run against an empty + * string. This is a behaviour consistent with previous + * releases. + */ + if (var->value == NULL) { + target = ""; + target_length = 0; + } else { + target = var->value; + target_length = var->value_len; + } + + /* IMP1 Can we tell the regex engine not to do any captures if we have no use for them? */ + rc = msc_regexec_capture(regex, target, target_length, ovector, 30, &my_error_msg); + if (rc < -1) { + *error_msg = apr_psprintf(msr->mp, "Regex execution failed: %s", my_error_msg); + return -1; + } + + /* Handle captured subexpressions. */ + if (rc > 0) { + int capture = 0; + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + int i; + + /* Are we supposed to store the captured subexpressions? */ + /* IMP1 Can we use a flag to avoid having to iterate through the list every time. */ + tarr = apr_table_elts(rule->actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + if (strcasecmp(action->metadata->name, "capture") == 0) { + capture = 1; + break; + } + } + + if (capture) { + int k; + + /* Use the available captures. */ + for(k = 0; k < rc; k++) { + msc_string *s = (msc_string *)apr_pcalloc(msr->mp, sizeof(msc_string)); + if (s == NULL) return -1; + s->name = apr_psprintf(msr->mp, "%i", k); + s->value = apr_pstrmemdup(msr->mp, + target + ovector[2*k], ovector[2*k + 1] - ovector[2*k]); + s->value_len = (ovector[2*k + 1] - ovector[2*k]); + if ((s->name == NULL)||(s->value == NULL)) return -1; + apr_table_setn(msr->tx_vars, s->name, (void *)s); + msr_log(msr, 9, "Adding regex subexpression to TXVARS (%i): %s", k, + log_escape_nq(msr->mp, s->value)); + } + + /* Unset the remaining ones (from previous invocations). */ + for(k = rc; k <= 9; k++) { + char buf[24]; + apr_snprintf(buf, sizeof(buf), "%i", k); + apr_table_unset(msr->tx_vars, buf); + } + } + } + + /* + if ( ((rc == PCRE_ERROR_NOMATCH)&&(rule->op_negated == 1)) + || ((rc != PCRE_ERROR_NOMATCH)&&(rule->op_negated == 0)) ) + { + */ + if (rc != PCRE_ERROR_NOMATCH) { /* Match. */ + char *pattern_escaped = log_escape(msr->mp, regex->pattern); + + /* This message will be logged. */ + if (strlen(pattern_escaped) > 252) { + *error_msg = apr_psprintf(msr->mp, "Pattern match \"%.252s ...\" at %s.", + pattern_escaped, var->name); + } else { + *error_msg = apr_psprintf(msr->mp, "Pattern match \"%s\" at %s.", + pattern_escaped, var->name); + } + + return 1; + } + + /* No match. */ + return 0; +} + +/* m */ + +static int msre_op_m_param_init(msre_rule *rule, char **error_msg) { + const apr_strmatch_pattern *compiled_pattern; + const char *pattern = rule->op_param; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* Compile pattern */ + compiled_pattern = apr_strmatch_precompile(rule->ruleset->mp, pattern, 1); + if (compiled_pattern == NULL) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Error compiling pattern: %s", pattern); + return 0; + } + + rule->op_param_data = (void *)compiled_pattern; + + return 1; /* OK */ +} + +static int msre_op_m_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) { + apr_strmatch_pattern *compiled_pattern = (apr_strmatch_pattern *)rule->op_param_data; + const char *target; + unsigned int target_length; + const char *rc; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (compiled_pattern == NULL) { + *error_msg = "Internal Error: strnmatch data is null."; + return -1; + } + + /* If the given target is null run against an empty + * string. This is a behaviour consistent with previous + * releases. + */ + if (var->value == NULL) { + target = ""; + target_length = 0; + } else { + target = var->value; + target_length = var->value_len; + } + + rc = apr_strmatch(compiled_pattern, target, target_length); + if (rc == NULL) { + /* No match. */ + return 0; + } + + *error_msg = apr_psprintf(msr->mp, "Pattern match \"%s\" at %s.", + log_escape(msr->mp, rule->op_param), var->name); + + /* Match. */ + return 1; +} + +#ifdef WITH_LIBXML2 + +/* validateDTD */ + +static int msre_op_validateDTD_init(msre_rule *rule, char **error_msg) { + /* ENH Verify here the file actually exists. */ + return 1; +} + +static int msre_op_validateDTD_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + xmlValidCtxtPtr cvp; + xmlDtdPtr dtd; + + if ((msr->xml == NULL)||(msr->xml->doc == NULL)) { + *error_msg = apr_psprintf(msr->mp, "XML document tree could not be found for " + "DTD validation."); + return -1; + } + + dtd = xmlParseDTD(NULL, rule->op_param); /* EHN support relative filenames */ + if (dtd == NULL) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed to load DTD: %s", rule->op_param); + return -1; + } + + cvp = xmlNewValidCtxt(); + if (cvp == NULL) { + *error_msg = "XML: Failed to create a validation context."; + xmlFreeDtd(dtd); + return -1; + } + + if (!xmlValidateDtd(cvp, msr->xml->doc, dtd)) { + *error_msg = "XML: DTD validation failed."; + xmlFreeValidCtxt(cvp); + xmlFreeDtd(dtd); + return 1; /* No match. */ + } + + msr_log(msr, 4, "XML: Successfully validated payload against DTD: %s", rule->op_param); + + xmlFreeValidCtxt(cvp); + xmlFreeDtd(dtd); + + /* Match. */ + return 0; +} + +/* validateSchema */ + +static int msre_op_validateSchema_init(msre_rule *rule, char **error_msg) { + /* ENH Verify here the file actually exists. */ + return 1; +} + +static int msre_op_validateSchema_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + xmlSchemaParserCtxtPtr parserCtx; + xmlSchemaValidCtxtPtr validCtx; + xmlSchemaPtr schema; + int rc; + + if ((msr->xml == NULL)||(msr->xml->doc == NULL)) { + *error_msg = apr_psprintf(msr->mp, "XML document tree could not be found for " + "Schema validation."); + return -1; + } + + parserCtx = xmlSchemaNewParserCtxt(rule->op_param); /* ENH support relative filenames */ + if (parserCtx == NULL) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed to load Schema from file: %s", + rule->op_param); + return -1; + } + + schema = xmlSchemaParse(parserCtx); + if (schema == NULL) { + *error_msg = apr_psprintf(msr->mp, "XML: Failed to load Schema: %s", rule->op_param); + xmlSchemaFreeParserCtxt(parserCtx); + return -1; + } + + validCtx = xmlSchemaNewValidCtxt(schema); + if (validCtx == NULL) { + *error_msg = "XML: Failed to create validation context."; + xmlSchemaFree(schema); + xmlSchemaFreeParserCtxt(parserCtx); + return -1; + } + + rc = xmlSchemaValidateDoc(validCtx, msr->xml->doc); + if (rc != 0) { + *error_msg = "XML: Schema validation failed."; + xmlSchemaFree(schema); + xmlSchemaFreeParserCtxt(parserCtx); + return 1; /* No match. */ + } + + msr_log(msr, 4, "XML: Successfully validated payload against Schema: %s", rule->op_param); + + xmlSchemaFree(schema); + xmlSchemaFreeValidCtxt(validCtx); + + return 0; +} + +#endif + +/* rbl */ + +static int msre_op_rbl_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, char **error_msg) { + unsigned int h0, h1, h2, h3; + char *name_to_check = NULL; + char *target = NULL; + apr_sockaddr_t *sa = NULL; + apr_status_t rc; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + /* ENH Add IPv6 support. */ + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + + /* Construct the host name we want to resolve. */ + if (sscanf(target, "%d.%d.%d.%d", &h0, &h1, &h2, &h3) == 4) { + /* IPv4 address */ + name_to_check = apr_psprintf(msr->mp, "%i.%i.%i.%i.%s", h3, h2, h1, h0, rule->op_param); + } else { + /* Assume the input is a domain name. */ + name_to_check = apr_psprintf(msr->mp, "%s.%s", target, rule->op_param); + } + + if (name_to_check == NULL) return -1; + + rc = apr_sockaddr_info_get(&sa, name_to_check, + APR_UNSPEC/*msr->r->connection->remote_addr->family*/, 0, 0, msr->mp); + if (rc == APR_SUCCESS) { + *error_msg = apr_psprintf(msr->r->pool, "RBL lookup of %s succeeded.", + log_escape_nq(msr->mp, name_to_check)); + return 1; /* Match. */ + } + + msr_log(msr, 5, "RBL lookup of %s failed.", log_escape_nq(msr->mp, name_to_check)); + + /* No match. */ + return 0; +} + +/* inspectFile */ +static int msre_op_inspectFile_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + char *script_output = NULL; + char const *argv[5]; + const char *approver_script = rule->op_param; + const char *target_file = apr_pstrmemdup(msr->mp, var->value, var->value_len); + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + msr_log(msr, 4, "Executing %s to inspect %s.", approver_script, target_file); + + argv[0] = approver_script; + argv[1] = target_file; + argv[2] = NULL; + + if (apache2_exec(msr, approver_script, (const char **)argv, &script_output) <= 0) { + *error_msg = apr_psprintf(msr->mp, "Execution of the approver script \"%s\" failed (invocation failed).", + log_escape(msr->mp, approver_script)); + return -1; + } + + if (script_output == NULL) { + *error_msg = apr_psprintf(msr->mp, "Execution of the approver script \"%s\" failed (no output).", + log_escape(msr->mp, approver_script)); + return -1; + } + + if (script_output[0] != '1') { + *error_msg = apr_psprintf(msr->mp, "File \"%s\" rejected by the approver script \"%s\": %s", + log_escape(msr->mp, target_file), log_escape(msr->mp, approver_script), + log_escape_nq(msr->mp, script_output)); + return 1; /* Match. */ + } + + /* No match. */ + return 0; +} + +/* validateByteRange */ + +static int msre_op_validateByteRange_init(msre_rule *rule, char **error_msg) { + char *p = NULL, *saveptr = NULL; + char *table = NULL, *data = NULL; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (rule->op_param == NULL) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Missing parameter for validateByteRange."); + return -1; + } + + /* Initialise. */ + data = apr_pstrdup(rule->ruleset->mp, rule->op_param); + rule->op_param_data = apr_pcalloc(rule->ruleset->mp, 32); + if ((data == NULL)||(rule->op_param_data == NULL)) return -1; + table = rule->op_param_data; + + /* Extract parameters and update table. */ + p = apr_strtok(data, ",", &saveptr); + while(p != NULL) { + char *s = strstr(p, "-"); + if (s == NULL) { + /* Single value. */ + int x = atoi(p); + if ((x < 0)||(x > 255)) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Invalid range value: %i", x); + return 0; + } + table[x>>3] = (table[x>>3] | (1 << (x & 0x7))); + } else { + /* Range. */ + int start = atoi(p); + int end = atoi(s + 1); + + if ((start < 0)||(start > 255)) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Invalid range start value: %i", + start); + return 0; + } + if ((end < 0)||(end > 255)) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Invalid range end value: %i", end); + return 0; + } + if (start > end) { + *error_msg = apr_psprintf(rule->ruleset->mp, "Invalid range: %i-%i", start, end); + return 0; + } + + while(start <= end) { + table[start >> 3] = (table[start >> 3] | (1 << (start & 0x7))); + start++; + } + } + + p = apr_strtok(NULL, ",", &saveptr); + } + + return 1; +} + +static int msre_op_validateByteRange_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + char *table = rule->op_param_data; + unsigned int i, count; + + if (error_msg == NULL) return -1; + *error_msg = NULL; + + if (table == NULL) { + *error_msg = apr_psprintf(msr->mp, "Internal Error: validateByteRange table not " + "initialised."); + return -1; + } + + /* Check every byte of the target to detect characters that are not allowed. */ + + count = 0; + for(i = 0; i < var->value_len; i++) { + int x = ((unsigned char *)var->value)[i]; + if (!(table[x >> 3] & (1 << (x & 0x7)))) { + msr_log(msr, 9, "Value %i outside range: %s", x, rule->op_param); + count++; + } + } + + if (count == 0) return 0; /* Valid - no match. */ + + *error_msg = apr_psprintf(msr->mp, "Found %i byte(s) outside range: %s.", + count, rule->op_param); + + return 1; /* Invalid - match.*/ +} + +/* validateUrlEncoding */ + +static int validate_url_encoding(const char *input, long int input_length) { + int i; + + if ((input == NULL)||(input_length < 0)) return -1; + + i = 0; + while (i < input_length) { + if (input[i] == '%') { + if (i + 2 >= input_length) { + /* Not enough bytes. */ + return -3; + } + else { + /* Here we only decode a %xx combination if it is valid, + * leaving it as is otherwise. + */ + char c1 = input[i + 1]; + char c2 = input[i + 2]; + + if ( (((c1 >= '0')&&(c1 <= '9')) || ((c1 >= 'a')&&(c1 <= 'f')) || ((c1 >= 'A')&&(c1 <= 'F'))) + && (((c2 >= '0')&&(c2 <= '9')) || ((c2 >= 'a')&&(c2 <= 'f')) || ((c2 >= 'A')&&(c2 <= 'F'))) ) + { + i += 3; + } else { + /* Non-hexadecimal characters used in encoding. */ + return -2; + } + } + } else { + i++; + } + } + + return 1; +} + +static int msre_op_validateUrlEncoding_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int rc = validate_url_encoding(var->value, var->value_len); + switch(rc) { + case 1 : + return 0; /* Encoding is valid, no match. */ + break; + case -2 : + *error_msg = apr_psprintf(msr->mp, "Invalid URL Encoding: Non-hexadecimal " + "digits used."); + return 1; /* Invalid, match. */ + break; + case -3 : + *error_msg = apr_psprintf(msr->mp, "Invalid URL Encoding: Not enough characters " + "at the end of input."); + return 1; /* Invalid, match. */ + break; + case -1 : + default : + *error_msg = apr_psprintf(msr->mp, "Invalid URL Encoding: Internal Error (rc = %i)", rc); + return -1; + break; + + } + + /* No match. */ + return 0; +} + +/* validateUtf8Encoding */ + +#define UNICODE_ERROR_CHARACTERS_MISSING -1 +#define UNICODE_ERROR_INVALID_ENCODING -2 +#define UNICODE_ERROR_OVERLONG_CHARACTER -3 + +static int detect_utf8_character(const char *p_read, unsigned int length) { + int unicode_len = 0; + unsigned int d = 0; + unsigned char c; + + if (p_read == NULL) return 0; + c = *p_read; + if (c == 0) return 0; + + if ((c & 0xE0) == 0xC0) { + /* two byte unicode */ + if (length < 2) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; + else + if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else { + unicode_len = 2; + d = ((c & 0x1F) << 6) | (*(p_read + 1) & 0x3F); + } + } + else if ((c & 0xF0) == 0xE0) { + /* three byte unicode */ + if (length < 3) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; + else + if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else { + unicode_len = 3; + d = ((c & 0x0F) << 12) | ((*(p_read + 1) & 0x3F) << 6) | (*(p_read + 2) & 0x3F); + } + } + else if ((c & 0xF8) == 0xF0) { + /* four byte unicode */ + if (length < 4) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; + else + if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else { + d = ((c & 0x07) << 18) | ((*(p_read + 1) & 0x3F) << 12) | ((*(p_read + 2) & 0x3F) < 6) | (*(p_read + 3) & 0x3F); + unicode_len = 4; + } + } + else if ((c & 0xFC) == 0xF8) { + /* five byte unicode */ + if (length < 5) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; + else + if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 4)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else { + d = ((c & 0x03) << 24) | ((*(p_read + 1) & 0x3F) << 18) | ((*(p_read + 2) & 0x3F) << 12) | ((*(p_read + 3) & 0x3F) << 6) | (*(p_read + 4) & 0x3F); + unicode_len = 5; + } + } + else if ((c & 0xFE) == 0xFC) { + /* six byte unicode */ + if (length < 6) unicode_len = UNICODE_ERROR_CHARACTERS_MISSING; + else + if (((*(p_read + 1)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 2)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 3)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 4)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else + if (((*(p_read + 5)) & 0xC0) != 0x80) unicode_len = UNICODE_ERROR_INVALID_ENCODING; + else { + d = ((c & 0x01) << 30) | ((*(p_read + 1) & 0x3F) << 24) | ((*(p_read + 2) & 0x3F) << 18) | ((*(p_read + 3) & 0x3F) << 12) | ((*(p_read + 4) & 0x3F) << 6) | (*(p_read + 5) & 0x3F); + unicode_len = 6; + } + } + + if ((unicode_len > 1)&&((d & 0x7F) == d)) { + unicode_len = UNICODE_ERROR_OVERLONG_CHARACTER; + } + + return unicode_len; +} + +static int msre_op_validateUtf8Encoding_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + unsigned int i, bytes_left; + + bytes_left = var->value_len; + for(i = 0; i < var->value_len; i++) { + int rc = detect_utf8_character(&var->value[i], bytes_left); + switch(rc) { + case UNICODE_ERROR_CHARACTERS_MISSING : + *error_msg = apr_psprintf(msr->mp, "Invalid UTF-8 encoding: not enough bytes in " + "character."); + return 1; + break; + case UNICODE_ERROR_INVALID_ENCODING : + *error_msg = apr_psprintf(msr->mp, "Invalid Unicode encoding: invalid byte value " + "in character."); + return 1; + break; + case UNICODE_ERROR_OVERLONG_CHARACTER : + *error_msg = apr_psprintf(msr->mp, "Invalid Unicode encoding: overlong " + "character detected."); + return 1; + break; + } + + bytes_left--; + } + + return 0; +} + +/* eq */ + +static int msre_op_eq_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int left, right; + char *target = NULL; + + if ((var->value == NULL)||(rule->op_param == NULL)) { + /* NULL values do not match anything. */ + return 0; + } + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + left = atoi(target); + right = atoi(rule->op_param); + + if (left != right) { + /* No match. */ + return 0; + } + else { + *error_msg = apr_psprintf(msr->mp, "Operator EQ match: %i.", right); + /* Match. */ + return 1; + } +} + +/* gt */ + +static int msre_op_gt_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int left, right; + char *target = NULL; + + if ((var->value == NULL)||(rule->op_param == NULL)) { + /* NULL values do not match anything. */ + return 0; + } + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + left = atoi(target); + right = atoi(rule->op_param); + + if (left <= right) { + /* No match. */ + return 0; + } + else { + *error_msg = apr_psprintf(msr->mp, "Operator GT match: %i.", right); + /* Match. */ + return 1; + } +} + +/* lt */ + +static int msre_op_lt_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int left, right; + char *target = NULL; + + if ((var->value == NULL)||(rule->op_param == NULL)) { + /* NULL values do not match anything. */ + return 0; + } + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + left = atoi(target); + right = atoi(rule->op_param); + + if (left >= right) { + /* No match. */ + return 0; + } + else { + *error_msg = apr_psprintf(msr->mp, "Operator LT match: %i.", right); + /* Match. */ + return 1; + } +} + +/* ge */ + +static int msre_op_ge_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int left, right; + char *target = NULL; + + if ((var->value == NULL)||(rule->op_param == NULL)) { + /* NULL values do not match anything. */ + return 0; + } + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + left = atoi(target); + right = atoi(rule->op_param); + + if (left < right) { + /* No match. */ + return 0; + } + else { + *error_msg = apr_psprintf(msr->mp, "Operator GE match: %i.", right); + /* Match. */ + return 1; + } +} + +/* le */ + +static int msre_op_le_execute(modsec_rec *msr, msre_rule *rule, msre_var *var, + char **error_msg) +{ + int left, right; + char *target = NULL; + + if ((var->value == NULL)||(rule->op_param == NULL)) { + /* NULL values do not match anything. */ + return 0; + } + + target = apr_pstrmemdup(msr->mp, var->value, var->value_len); + if (target == NULL) return -1; + left = atoi(target); + right = atoi(rule->op_param); + + if (left > right) { + /* No match. */ + return 0; + } + else { + *error_msg = apr_psprintf(msr->mp, "Operator LE match: %i.", right); + /* Match. */ + return 1; + } +} + +/* ------------------------------------------------------------------------------- */ + +/** + * + */ +void msre_engine_register_default_operators(msre_engine *engine) { + /* unconditionalMatch */ + msre_engine_op_register(engine, + "unconditionalMatch", + NULL, + msre_op_unconditionalmatch_execute + ); + + /* rx */ + msre_engine_op_register(engine, + "rx", + msre_op_rx_param_init, + msre_op_rx_execute + ); + + /* m */ + msre_engine_op_register(engine, + "m", + msre_op_m_param_init, + msre_op_m_execute + ); + + #ifdef WITH_LIBXML2 + + /* validateDTD */ + msre_engine_op_register(engine, + "validateDTD", + msre_op_validateDTD_init, + msre_op_validateDTD_execute + ); + + /* validateSchema */ + msre_engine_op_register(engine, + "validateSchema", + msre_op_validateSchema_init, + msre_op_validateSchema_execute + ); + + #endif + + /* rbl */ + msre_engine_op_register(engine, + "rbl", + NULL, /* ENH init function to validate DNS server */ + msre_op_rbl_execute + ); + + /* inspectFile */ + msre_engine_op_register(engine, + "inspectFile", + NULL, + msre_op_inspectFile_execute + ); + + /* validateByteRange */ + msre_engine_op_register(engine, + "validateByteRange", + msre_op_validateByteRange_init, + msre_op_validateByteRange_execute + ); + + /* validateUrlEncoding */ + msre_engine_op_register(engine, + "validateUrlEncoding", + NULL, + msre_op_validateUrlEncoding_execute + ); + + /* validateUtf8Encoding */ + msre_engine_op_register(engine, + "validateUtf8Encoding", + NULL, + msre_op_validateUtf8Encoding_execute + ); + + /* eq */ + msre_engine_op_register(engine, + "eq", + NULL, + msre_op_eq_execute + ); + + /* gt */ + msre_engine_op_register(engine, + "gt", + NULL, + msre_op_gt_execute + ); + + /* lt */ + msre_engine_op_register(engine, + "lt", + NULL, + msre_op_lt_execute + ); + + /* le */ + msre_engine_op_register(engine, + "le", + NULL, + msre_op_le_execute + ); + + /* ge */ + msre_engine_op_register(engine, + "ge", + NULL, + msre_op_ge_execute + ); +} diff --git a/apache2/re_tfns.c b/apache2/re_tfns.c new file mode 100644 index 00000000..8bea8846 --- /dev/null +++ b/apache2/re_tfns.c @@ -0,0 +1,512 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re_tfns.c,v 1.3 2006/12/04 12:00:24 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include + +#include "apr_md5.h" +#include "apr_sha1.h" +#include "apr_base64.h" + +#include "re.h" +#include "msc_util.h" + +/* lowercase */ + +static int msre_fn_lowercase_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i; + int changed = 0; + + if (rval == NULL) return -1; + *rval = NULL; + + i = 0; + while(i < input_len) { + int x = input[i]; + input[i] = tolower(x); + if (x != input[i]) changed = 1; + i++; + } + + *rval = (char *)input; + *rval_len = input_len; + + return changed; +} + +/* removeNulls */ + +static int msre_fn_removeNulls_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i, j; + int changed = 0; + + i = j = 0; + while(i < input_len) { + if (input[i] == '\0') { + changed = 1; + } else { + input[j] = input[i]; + j++; + } + i++; + } + + *rval = (char *)input; + *rval_len = j; + + return changed; +} + +/* replaceNulls */ + +static int msre_fn_replaceNulls_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i; + int changed = 0; + + if (rval == NULL) return -1; + *rval = NULL; + + i = 0; + while(i < input_len) { + if (input[i] == '\0') { + changed = 1; + input[i] = ' '; + } + i++; + } + + *rval = (char *)input; + *rval_len = input_len; + + return changed; +} + +/* compressWhitespace */ + +static int msre_fn_compressWhitespace_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i, j, count; + int changed = 0; + + i = j = count = 0; + while(i < input_len) { + if (isspace(input[i])||(input[i] == NBSP)) { + changed = 1; + count++; + } else { + if (count) { + input[j] = ' '; + count = 0; + j++; + } + input[j] = input[i]; + j++; + } + i++; + } + + if (count) { + input[j] = ' '; + j++; + } + + *rval = (char *)input; + *rval_len = j; + + return changed; +} + +/* removeWhitespace */ + +static int msre_fn_removeWhitespace_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i, j; + int changed = 0; + + i = j = 0; + while(i < input_len) { + if (isspace(input[i])||(input[i] == NBSP)) { + /* do nothing */ + changed = 1; + } else { + input[j] = input[i]; + j++; + } + i++; + } + + *rval = (char *)input; + *rval_len = j; + + return changed; +} + +/* replaceComments */ + +static int msre_fn_replaceComments_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int i, j, incomment; + int changed = 0; + + i = j = incomment = 0; + while(i < input_len) { + if (incomment == 0) { + if ((input[i] == '/')&&(i + 1 < input_len)&&(input[i + 1] == '*')) { + changed = 1; + incomment = 1; + i += 2; + } else { + input[j] = input[i]; + i++; + j++; + } + } else { + if ((input[i] == '*')&&(i + 1 < input_len)&&(input[i + 1] == '/')) { + incomment = 0; + i += 2; + input[j] = ' '; + j++; + } else { + i++; + } + } + } + + *rval = (char *)input; + *rval_len = j; + + return changed; +} + +/* urlDecode */ + +static int msre_fn_urlDecode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int length; + int invalid_count; + + length = urldecode_nonstrict_inplace_ex(input, input_len, &invalid_count); + *rval = (char *)input; + *rval_len = length; + + return (*rval_len == input_len ? 0 : 1); +} + +/* urlDecodeUni */ + +static int msre_fn_urlDecodeUni_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + long int length; + + length = urldecode_uni_nonstrict_inplace_ex(input, input_len); + *rval = (char *)input; + *rval_len = length; + + return (*rval_len == input_len ? 0 : 1); +} + +/* urlEncode */ + +static int msre_fn_urlEncode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval = url_encode(mptmp, input, input_len); + *rval_len = strlen(*rval); + + return (*rval_len == input_len ? 0 : 1); +} + +/* base64Encode */ + +static int msre_fn_base64Encode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = apr_base64_encode_len(input_len); /* returns len with NULL byte included */ + *rval = apr_palloc(mptmp, *rval_len); + apr_base64_encode(*rval, input, input_len); + (*rval_len)--; + + return 1; +} + +/* base64Decode */ + +static int msre_fn_base64Decode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = apr_base64_decode_len(input); /* returns len with NULL byte included */ + *rval = apr_palloc(mptmp, *rval_len); + apr_base64_decode(*rval, input); + (*rval_len)--; + + return 1; +} + +/* md5 */ + +static int msre_fn_md5_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + unsigned char digest[APR_MD5_DIGESTSIZE]; + + apr_md5(digest, input, input_len); + + *rval_len = APR_MD5_DIGESTSIZE; + *rval = apr_pstrmemdup(mptmp, digest, APR_MD5_DIGESTSIZE); + + return 1; +} + +/* sha1 */ + +static int msre_fn_sha1_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + unsigned char digest[APR_SHA1_DIGESTSIZE]; + apr_sha1_ctx_t context; + + apr_sha1_init(&context); + apr_sha1_update(&context, input, input_len); + apr_sha1_final(digest, &context); + + *rval_len = APR_SHA1_DIGESTSIZE; + *rval = apr_pstrmemdup(mptmp, digest, APR_SHA1_DIGESTSIZE); + + return 1; +} + +/* hexDecode */ + +static int msre_fn_hexDecode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = hex2bytes_inplace(input, input_len); + *rval = input; + + return 1; +} + +/* hexEncode */ + +static int msre_fn_hexEncode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval = bytes2hex(mptmp, input, input_len); + *rval_len = strlen(*rval); + + return 1; +} + +/* htmlEntityDecode */ + +static int msre_fn_htmlEntityDecode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = html_entities_decode_inplace(mptmp, input, input_len); + *rval = input; + + return (*rval_len == input_len ? 0 : 1); +} + +/* escapeSeqDecode */ + +static int msre_fn_escapeSeqDecode_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = ansi_c_sequences_decode_inplace(input, input_len); + *rval = input; + + return (*rval_len == input_len ? 0 : 1); +} + +/* normalisePath */ + +static int msre_fn_normalisePath_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = normalise_path_inplace(input, input_len, 0); + *rval = input; + + return (*rval_len == input_len ? 0 : 1); +} + +/* normalisePathWin */ + +static int msre_fn_normalisePathWin_execute(apr_pool_t *mptmp, unsigned char *input, + long int input_len, char **rval, long int *rval_len) +{ + *rval_len = normalise_path_inplace(input, input_len, 1); + *rval = input; + + return (*rval_len == input_len ? 0 : 1); +} + +/* ------------------------------------------------------------------------------ */ + +/** + * Registers one transformation function with the engine. + */ +void msre_engine_tfn_register(msre_engine *engine, const char *name, + FN_TFN_EXECUTE(execute)) +{ + msre_tfn_metadata *metadata = (msre_tfn_metadata *)apr_pcalloc(engine->mp, + sizeof(msre_tfn_metadata)); + if (metadata == NULL) return; + + metadata->name = name; + metadata->execute = execute; + + apr_table_setn(engine->tfns, name, (void *)metadata); +} + +/** + * Returns transformation function metadata given a name. + */ +msre_tfn_metadata *msre_engine_tfn_resolve(msre_engine *engine, const char *name) { + return (msre_tfn_metadata *)apr_table_get(engine->tfns, name); +} + +/** + * Register the default transformation functions. + */ +void msre_engine_register_default_tfns(msre_engine *engine) { + + /* none */ + msre_engine_tfn_register(engine, + "none", + NULL + ); + + /* base64Decode */ + msre_engine_tfn_register(engine, + "base64Decode", + msre_fn_base64Decode_execute + ); + + /* base64Encode */ + msre_engine_tfn_register(engine, + "base64Encode", + msre_fn_base64Encode_execute + ); + + /* compressWhitespace */ + msre_engine_tfn_register(engine, + "compressWhitespace", + msre_fn_compressWhitespace_execute + ); + + /* escapeSeqDecode */ + msre_engine_tfn_register(engine, + "escapeSeqDecode", + msre_fn_escapeSeqDecode_execute + ); + + /* hexDecode */ + msre_engine_tfn_register(engine, + "hexDecode", + msre_fn_hexDecode_execute + ); + + /* hexEncode */ + msre_engine_tfn_register(engine, + "hexEncode", + msre_fn_hexEncode_execute + ); + + /* htmlEntityDecode */ + msre_engine_tfn_register(engine, + "htmlEntityDecode", + msre_fn_htmlEntityDecode_execute + ); + + /* lowercase */ + msre_engine_tfn_register(engine, + "lowercase", + msre_fn_lowercase_execute + ); + + /* md5 */ + msre_engine_tfn_register(engine, + "md5", + msre_fn_md5_execute + ); + + /* normalisePath */ + msre_engine_tfn_register(engine, + "normalisePath", + msre_fn_normalisePath_execute + ); + + /* normalisePathWin */ + msre_engine_tfn_register(engine, + "normalisePathWin", + msre_fn_normalisePathWin_execute + ); + + /* removeWhitespace */ + msre_engine_tfn_register(engine, + "removeWhitespace", + msre_fn_removeWhitespace_execute + ); + + /* removeNulls */ + msre_engine_tfn_register(engine, + "removeNulls", + msre_fn_removeNulls_execute + ); + + /* replaceNulls */ + msre_engine_tfn_register(engine, + "replaceNulls", + msre_fn_replaceNulls_execute + ); + + /* replaceComments */ + msre_engine_tfn_register(engine, + "replaceComments", + msre_fn_replaceComments_execute + ); + + /* sha1 */ + msre_engine_tfn_register(engine, + "sha1", + msre_fn_sha1_execute + ); + + /* urlDecode */ + msre_engine_tfn_register(engine, + "urlDecode", + msre_fn_urlDecode_execute + ); + + /* urlDecodeUni */ + msre_engine_tfn_register(engine, + "urlDecodeUni", + msre_fn_urlDecodeUni_execute + ); + + /* urlEncode */ + msre_engine_tfn_register(engine, + "urlEncode", + msre_fn_urlEncode_execute + ); +} diff --git a/apache2/re_variables.c b/apache2/re_variables.c new file mode 100644 index 00000000..a7cf29e2 --- /dev/null +++ b/apache2/re_variables.c @@ -0,0 +1,2443 @@ +/* + * ModSecurity for Apache 2.x, http://www.modsecurity.org/ + * Copyright (c) 2004-2006 Thinking Stone (http://www.thinkingstone.com) + * + * $Id: re_variables.c,v 1.7 2007/01/23 16:08:15 ivanr Exp $ + * + * You should have received a copy of the licence along with this + * program (stored in the file "LICENSE"). If the file is missing, + * or if you have any other questions related to the licence, please + * write to Thinking Stone at contact@thinkingstone.com. + * + */ +#include "http_core.h" + +#include "modsecurity.h" +#include "apache2.h" +#include "re.h" +#include "msc_util.h" + +#ifdef WITH_LIBXML2 +#include "libxml/xpathInternals.h" +#endif + +/** + * Generates a variable from a string and a length. + */ +static int var_simple_generate_ex(msre_var *var, apr_table_t *vartab, apr_pool_t *mptmp, + const char *value, int value_len) +{ + msre_var *rvar = NULL; + + if (value == NULL) return 0; + + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = value; + rvar->value_len = value_len; + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/** + * Generates a variable from a NULL-terminated string. + */ +static int var_simple_generate(msre_var *var, apr_table_t *vartab, apr_pool_t *mptmp, + const char *value) +{ + if (value == NULL) return 0; + return var_simple_generate_ex(var, vartab, mptmp, value, strlen(value)); +} + +/** + * Validate that a target parameter is valid. We only need to take + * care of the case when the parameter is a regular expression. + */ +static char *var_generic_list_validate(msre_ruleset *ruleset, msre_var *var) { + /* It's OK if there's no parameter. */ + if (var->param == NULL) return NULL; + + /* Is it a regular expression? */ + if ((strlen(var->param) > 2)&&(var->param[0] == '/') + &&(var->param[strlen(var->param) - 1] == '/')) + { /* Regex. */ + msc_regex_t *regex = NULL; + const char *errptr = NULL; + const char *pattern = NULL; + int erroffset; + + pattern = apr_pstrmemdup(ruleset->mp, var->param + 1, strlen(var->param + 1) - 1); + if (pattern == NULL) return FATAL_ERROR; + + regex = msc_pregcomp(ruleset->mp, pattern, PCRE_DOTALL | PCRE_CASELESS, &errptr, &erroffset); + if (regex == NULL) { + return apr_psprintf(ruleset->mp, "Error compiling pattern (pos %i): %s", + erroffset, errptr); + } + + /* Store the compiled regex for later. */ + var->param_data = regex; + } + + /* Simple string */ + return NULL; +} + +/* Custom parameter validation functions */ + +/* ARGS */ + +static int var_args_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + /* Loop through the arguments. */ + arr = apr_table_elts(msr->arguments); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_arg *arg = (msc_arg *)te[i].val; + int match = 0; + + /* Figure out if we want to include this argument. */ + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + /* Run the regex against the argument name. */ + if (!(msc_regexec((msc_regex_t *)var->param_data, arg->name, + arg->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(arg->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = arg->value; + rvar->value_len = arg->value_len; + rvar->name = apr_psprintf(mptmp, "ARGS:%s", log_escape_nq(mptmp, arg->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* ARGS_COMBINED_SIZE */ + +static int var_args_combined_size_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + unsigned int combined_size = 0; + int i; + msre_var *rvar = NULL; + + arr = apr_table_elts(msr->arguments); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_arg *arg = (msc_arg *)te[i].val; + combined_size += arg->name_len; + combined_size += arg->value_len; + } + + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%u", combined_size); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* ARGS_NAMES */ + +static int var_args_names_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->arguments); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_arg *arg = (msc_arg *)te[i].val; + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, arg->name, + arg->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(arg->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = arg->name; + rvar->value_len = arg->name_len; + rvar->name = apr_psprintf(mptmp, "ARGS_NAMES:%s", log_escape_nq(mptmp, arg->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* RULE */ + +static int var_rule_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_actionset *actionset = NULL; + + if (rule == NULL) return 0; + actionset = rule->actionset; + if (rule->chain_starter != NULL) actionset = rule->chain_starter->actionset; + + if ((strcasecmp(var->param, "id") == 0)&&(actionset->id != NULL)) { + return var_simple_generate(var, vartab, mptmp, actionset->id); + } else + if ((strcasecmp(var->param, "rev") == 0)&&(actionset->rev != NULL)) { + return var_simple_generate(var, vartab, mptmp, actionset->rev); + } else + if ((strcasecmp(var->param, "severity") == 0)&&(actionset->severity != -1)) { + char *value = apr_psprintf(mptmp, "%i", actionset->severity); + return var_simple_generate(var, vartab, mptmp, value); + } else + if ((strcasecmp(var->param, "msg") == 0)&&(actionset->msg != NULL)) { + return var_simple_generate(var, vartab, mptmp, actionset->msg); + } + + return 0; +} + +/* ENV */ + +static char *var_env_validate(msre_ruleset *ruleset, msre_var *var) { + if ((strlen(var->param) > 2)&&(var->param[0] == '/') + &&(var->param[strlen(var->param) - 1] == '/')) + { + return apr_psprintf(ruleset->mp, "Regular expressions not supported in ENV."); + } + return NULL; +} + +static int var_env_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = get_env_var(msr->r, (char *)var->param); + if (value != NULL) { + return var_simple_generate(var, vartab, mptmp, value); + } + return 0; +} + +/* REQUEST_URI_RAW */ + +static int var_request_uri_raw_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->r->unparsed_uri); +} + +/* REQUEST_URI */ + +static int var_request_uri_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = NULL; + + if (msr->r->parsed_uri.query == NULL) value = msr->r->parsed_uri.path; + else value = apr_pstrcat(mptmp, msr->r->parsed_uri.path, "?", msr->r->parsed_uri.query, NULL); + + return var_simple_generate(var, vartab, mptmp, value); +} + +/* REQBODY_PROCESSOR */ + +static int var_reqbody_processor_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + if (msr->msc_reqbody_processor == NULL) { + rvar->value = apr_pstrdup(mptmp, ""); + rvar->value_len = 0; + } else { + rvar->value = apr_pstrdup(mptmp, msr->msc_reqbody_processor); + rvar->value_len = strlen(rvar->value); + } + + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* REQBODY_ERROR */ + +static int var_reqbody_processor_error_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = apr_psprintf(mptmp, "%i", msr->msc_reqbody_error); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* REQBODY_ERROR_MSG */ + +static int var_reqbody_processor_error_msg_generate(modsec_rec *msr, msre_var *var, + msre_rule *rule, apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + if (msr->msc_reqbody_error_msg == NULL) { + rvar->value = apr_pstrdup(mptmp, ""); + rvar->value_len = 0; + } else { + rvar->value = apr_psprintf(mptmp, "%s", msr->msc_reqbody_error_msg); + rvar->value_len = strlen(rvar->value); + } + + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +#ifdef WITH_LIBXML2 + +/* XML */ + +static char *var_xml_validate(msre_ruleset *ruleset, msre_var *var) { + /* It's OK if there's no parameter. */ + if (var->param == NULL) return NULL; + + /* ENH validate XPath expression in advance. */ + + return NULL; +} + +static int var_xml_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *tarr; + const apr_table_entry_t *telts; + xmlXPathContextPtr xpathCtx; + xmlXPathObjectPtr xpathObj; + xmlNodeSetPtr nodes; + const xmlChar* xpathExpr = NULL; + int i, count; + + /* Is there an XML document tree at all? */ + if ((msr->xml == NULL)||(msr->xml->doc == NULL)) { + /* Sorry, we've got nothing to give! */ + return 0; + } + + if (var->param == NULL) { + /* Invocation without an XPath expression makes sense + * with functions that manipulate the document tree. + */ + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = apr_pstrdup(mptmp, "[XML document tree]"); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; + } + + /* Process the XPath expression. */ + + count = 0; + xpathExpr = var->param; + + xpathCtx = xmlXPathNewContext(msr->xml->doc); + if (xpathCtx == NULL) { + msr_log(msr, 1, "XML: Unable to create new XPath context."); + return -1; + } + + /* Look through the actionset of the associated rule + * for the namespace information. Register them if any are found. + */ + tarr = apr_table_elts(rule->actionset->actions); + telts = (const apr_table_entry_t*)tarr->elts; + for (i = 0; i < tarr->nelts; i++) { + msre_action *action = (msre_action *)telts[i].val; + + if (strcasecmp(action->metadata->name, "xmlns") == 0) { + char *prefix, *href; + + if (parse_name_eq_value(mptmp, action->param, &prefix, &href) < 0) return -1; + if ((prefix == NULL)||(href == NULL)) return -1; + + if(xmlXPathRegisterNs(xpathCtx, prefix, href) != 0) { + msr_log(msr, 1, "Failed to register XML namespace href \"%s\" prefix \"%s\".", + log_escape(mptmp, prefix), log_escape(mptmp, href)); + return -1; + } + + msr_log(msr, 4, "Registered XML namespace href \"%s\" prefix \"%s\".", + log_escape(mptmp, prefix), log_escape(mptmp, href)); + } + } + + /* Initialise XPath expression. */ + xpathObj = xmlXPathEvalExpression(xpathExpr, xpathCtx); + if (xpathObj == NULL) { + msr_log(msr, 1, "XML: Unable to evaluate xpath expression."); + xmlXPathFreeContext(xpathCtx); + return -1; + } + + /* Evaluate XPath expression. */ + nodes = xpathObj->nodesetval; + if (nodes == NULL) { + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); + return 0; + } + + /* Create one variable for each node in the result. */ + for(i = 0; i < nodes->nodeNr; i++) { + msre_var *rvar = NULL; + char *content = NULL; + + content = xmlNodeGetContent(nodes->nodeTab[i]); + if (content != NULL) { + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_pstrdup(mptmp, content); + xmlFree(content); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + xmlXPathFreeObject(xpathObj); + xmlXPathFreeContext(xpathCtx); + + return count; +} +#endif + +/* WEBSERVER_ERROR_LOG */ + +static int var_webserver_error_log_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + int i, count = 0; + + for(i = 0; i < msr->error_messages->nelts; i++) { + error_message *em = (((error_message**)msr->error_messages->elts)[i]); + char *fem = NULL; + + fem = format_error_log_message(mptmp, em); + if (fem != NULL) { + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_pstrdup(mptmp, fem); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* REMOTE_ADDR */ + +static int var_remote_addr_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->remote_addr); +} + +/* REMOTE_HOST */ + +static int var_remote_host_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value1 = ap_get_remote_host(msr->r->connection, msr->r->per_dir_config, + REMOTE_NAME, NULL); + return var_simple_generate(var, vartab, mptmp, value1); +} + +/* REMOTE_PORT */ + +static int var_remote_port_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = apr_psprintf(mptmp, "%i", msr->remote_port); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* REMOTE_USER */ + +static int var_remote_user_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->remote_user); +} + +/* TX */ + +static int var_tx_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->tx_vars); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "TX:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* IP */ + +static int var_ip_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + apr_table_t *target_col = NULL; + + target_col = (apr_table_t *)apr_table_get(msr->collections, "ip"); + if (target_col == NULL) return 0; + + arr = apr_table_elts(target_col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "IP:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* SESSION */ + +static int var_session_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + apr_table_t *target_col = NULL; + + target_col = (apr_table_t *)apr_table_get(msr->collections, "session"); + if (target_col == NULL) return 0; + + arr = apr_table_elts(target_col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional inclusion. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "SESSION:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* USER */ + +static int var_user_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + apr_table_t *target_col = NULL; + + target_col = (apr_table_t *)apr_table_get(msr->collections, "user"); + if (target_col == NULL) return 0; + + arr = apr_table_elts(target_col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional match. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "USER:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* GLOBAL */ + +static int var_global_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + apr_table_t *target_col = NULL; + + target_col = (apr_table_t *)apr_table_get(msr->collections, "global"); + if (target_col == NULL) return 0; + + arr = apr_table_elts(target_col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional match. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "GLOBAL:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* RESOURCE */ + +static int var_resource_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + apr_table_t *target_col = NULL; + + target_col = (apr_table_t *)apr_table_get(msr->collections, "resource"); + if (target_col == NULL) return 0; + + arr = apr_table_elts(target_col); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + msc_string *str = (msc_string *)te[i].val; + int match; + + /* Figure out if we want to include this variable. */ + match = 0; + if (var->param == NULL) match = 1; /* Unconditional match. */ + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, str->name, + str->name_len, &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(str->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = str->value; + rvar->value_len = str->value_len; + rvar->name = apr_psprintf(mptmp, "RESOURCE:%s", log_escape_nq(mptmp, str->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* FILES_TMPNAMES */ + +static int var_files_tmpnames_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + multipart_part **parts = NULL; + int i, count = 0; + + if (msr->mpd == NULL) return 0; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if ((parts[i]->type == MULTIPART_FILE)&&(parts[i]->tmp_file_name != NULL)) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, parts[i]->name, + strlen(parts[i]->name), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(parts[i]->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = parts[i]->tmp_file_name; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "FILES_TMPNAMES:%s", + log_escape_nq(mptmp, parts[i]->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + } + + return count; +} + +/* FILES */ + +static int var_files_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + multipart_part **parts = NULL; + int i, count = 0; + + if (msr->mpd == NULL) return 0; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FILE) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, parts[i]->name, + strlen(parts[i]->name), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(parts[i]->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = parts[i]->filename; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "FILES:%s", + log_escape_nq(mptmp, parts[i]->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + } + + return count; +} + +/* FILES_SIZES */ + +static int var_files_sizes_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + multipart_part **parts = NULL; + int i, count = 0; + + if (msr->mpd == NULL) return 0; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FILE) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, parts[i]->name, + strlen(parts[i]->name), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(parts[i]->name, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = apr_psprintf(mptmp, "%u", parts[i]->tmp_file_size); + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "FILES_SIZES:%s", + log_escape_nq(mptmp, parts[i]->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + } + + return count; +} + +/* FILES_NAMES */ + +static int var_files_names_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + multipart_part **parts = NULL; + int i, count = 0; + + if (msr->mpd == NULL) return 0; + + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FILE) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = parts[i]->name; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "FILES_NAMES:%s", + log_escape_nq(mptmp, parts[i]->name)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* FILES_COMBINED_SIZE */ + +static int var_files_combined_size_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + multipart_part **parts = NULL; + msre_var *rvar = NULL; + unsigned int combined_size = 0; + int i; + + if (msr->mpd != NULL) { + parts = (multipart_part **)msr->mpd->parts->elts; + for(i = 0; i < msr->mpd->parts->nelts; i++) { + if (parts[i]->type == MULTIPART_FILE) { + combined_size += parts[i]->tmp_file_size; + } + } + } + + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%u", combined_size); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME */ + +static int var_time_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d%02d%02d%02d%02d%02d%02d", + (tm->tm_year / 100) + 19, (tm->tm_year % 100), + tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, + tm->tm_sec); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_YEAR */ + +static int var_time_year_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d%02d", + (tm->tm_year / 100) + 19, + tm->tm_year % 100); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_WDAY */ + +static int var_time_wday_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%d", tm->tm_wday); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_SEC */ + +static int var_time_sec_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d", tm->tm_sec); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_MIN */ + +static int var_time_min_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d", tm->tm_min); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_HOUR */ +static int var_time_hour_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d", tm->tm_hour); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_MON */ + +static int var_time_mon_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d", tm->tm_mon + 1); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_DAY */ + +static int var_time_day_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%02d", tm->tm_mday); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* TIME_EPOCH */ + +static int var_time_epoch_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + msre_var *rvar = NULL; + struct tm *tm; + time_t tc; + + tc = time(NULL); + tm = localtime(&tc); + rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + rvar->value = apr_psprintf(mptmp, "%i", (int)tc); + rvar->value_len = strlen(rvar->value); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + return 1; +} + +/* QUERY_STRING */ + +static int var_query_string_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->query_string); +} + +/* REQUEST_BASENAME */ + +static int var_request_basename_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = file_basename(mptmp, msr->r->parsed_uri.path); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* REQUEST_BODY */ + +static int var_request_body_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + if (msr->msc_reqbody_buffer != NULL) { + return var_simple_generate_ex(var, vartab, mptmp, + msr->msc_reqbody_buffer, msr->msc_reqbody_length); + } + return 0; +} + +/* REQUEST_COOKIES */ + +static int var_request_cookies_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->request_cookies); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].val; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "REQUEST_COOKIES:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* REQUEST_COOKIES_NAMES */ + +static int var_request_cookies_names_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->request_cookies); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].key; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "REQUEST_COOKIES_NAMES:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* REQUEST_HEADERS */ + +static int var_request_headers_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->request_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].val; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "REQUEST_HEADERS:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* REQUEST_HEADERS_NAMES */ + +static int var_request_headers_names_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->request_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].key; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "REQUEST_HEADERS_NAMES:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* REQUEST_FILENAME */ + +static int var_request_filename_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = msr->r->parsed_uri.path; + + if (value != NULL) { + int invalid_count = 0; + urldecode_nonstrict_inplace_ex(value, strlen(value), &invalid_count); + } + + return var_simple_generate(var, vartab, mptmp, value); +} + +/* REQUEST_LINE */ + +static int var_request_line_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->request_line); +} + +/* REQUEST_METHOD */ + +static int var_request_method_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->request_method); +} + +/* REQUEST_PROTOCOL */ + +static int var_request_protocol_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->request_protocol); +} + +/* SERVER_ADDR */ + +static int var_server_addr_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->local_addr); +} + +/* SERVER_NAME */ + +static int var_server_name_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + return var_simple_generate(var, vartab, mptmp, msr->hostname); +} + +/* SERVER_PORT */ + +static int var_server_port_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = apr_psprintf(mptmp, "%i", msr->local_port); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_BASENAME */ + +static int var_script_basename_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = file_basename(mptmp, msr->r->filename); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_FILENAME */ + +static int var_script_filename_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = msr->r->filename; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_GID */ + +static int var_script_gid_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = apr_psprintf(mptmp, "%i", msr->r->finfo.group); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_GROUPNAME */ + +static int var_script_groupname_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = NULL; + if (apr_gid_name_get(&value, msr->r->finfo.group, mptmp) == APR_SUCCESS) { + return var_simple_generate(var, vartab, mptmp, value); + } + return 0; +} + +/* SCRIPT_MODE */ + +static int var_script_mode_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = apr_psprintf(mptmp, "%04x", msr->r->finfo.protection); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_UID */ + +static int var_script_uid_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = apr_psprintf(mptmp, "%i", msr->r->finfo.user); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SCRIPT_USERNAME */ + +static int var_script_username_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = NULL; + if (apr_uid_name_get(&value, msr->r->finfo.user, mptmp) == APR_SUCCESS) { + return var_simple_generate(var, vartab, mptmp, value); + } + return 0; +} + +/* AUTH_TYPE */ + +static int var_auth_type_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + char *value = msr->r->ap_auth_type; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* PATH_INFO */ + +static int var_path_info_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->r->path_info; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* RESPONSE_BODY */ + +static int var_response_body_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + if (msr->resbody_data != NULL) { + return var_simple_generate_ex(var, vartab, mptmp, + msr->resbody_data, msr->resbody_length); + } + + return 0; +} + +/* RESPONSE_HEADERS */ + +static int var_response_headers_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + if (msr->response_headers == NULL) return 0; + + arr = apr_table_elts(msr->response_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].val; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "RESPONSE_HEADERS:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* RESPONSE_HEADERS_NAMES */ + +static int var_response_headers_names_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const apr_array_header_t *arr = NULL; + const apr_table_entry_t *te = NULL; + int i, count = 0; + + arr = apr_table_elts(msr->response_headers); + te = (apr_table_entry_t *)arr->elts; + for (i = 0; i < arr->nelts; i++) { + int match = 0; + + /* Figure out if we want to include this variable. */ + if (var->param == NULL) match = 1; + else { + if (var->param_data != NULL) { /* Regex. */ + char *my_error_msg = NULL; + if (!(msc_regexec((msc_regex_t *)var->param_data, te[i].key, + strlen(te[i].key), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; + } else { /* Simple comparison. */ + if (strcasecmp(te[i].key, var->param) == 0) match = 1; + } + } + + /* If we had a match add this argument to the collection. */ + if (match) { + msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); + + rvar->value = te[i].key; + rvar->value_len = strlen(rvar->value); + rvar->name = apr_psprintf(mptmp, "RESPONSE_HEADERS_NAMES:%s", + log_escape_nq(mptmp, te[i].key)); + apr_table_addn(vartab, rvar->name, (void *)rvar); + + count++; + } + } + + return count; +} + +/* STATUS_LINE */ + +static int var_status_line_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->status_line; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* RESPONSE_PROTOCOL */ + +static int var_response_protocol_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->response_protocol; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* RESPONSE_STATUS */ + +static int var_response_status_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = apr_psprintf(mptmp, "%i", msr->response_status); + return var_simple_generate(var, vartab, mptmp, value); +} + +/* USERID */ + +static int var_userid_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->userid; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* SESSIONID */ + +static int var_sessionid_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->sessionid; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* WEBAPPID */ + +static int var_webappid_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, + apr_table_t *vartab, apr_pool_t *mptmp) +{ + const char *value = msr->txcfg->webappid; + return var_simple_generate(var, vartab, mptmp, value); +} + +/* ---------------------------------------------- */ + +/** + * + */ +static void msre_engine_variable_register(msre_engine *engine, const char *name, + unsigned int type, unsigned int argc_min, unsigned int argc_max, + FN_VAR_VALIDATE(validate), FN_VAR_GENERATE(generate), + unsigned int is_cacheable, unsigned int availability) +{ + msre_var_metadata *metadata = (msre_var_metadata *)apr_pcalloc(engine->mp, + sizeof(msre_var_metadata)); + if (metadata == NULL) return; + + metadata->name = name; + metadata->type = type; + metadata->argc_min = argc_min; + metadata->argc_max = argc_max; + metadata->validate = validate; + metadata->generate = generate; + metadata->is_cacheable = is_cacheable; + metadata->availability = availability; + + apr_table_setn(engine->variables, name, (void *)metadata); +} + +/** + * + */ +void msre_engine_register_default_variables(msre_engine *engine) { + + /* ARGS */ + msre_engine_variable_register(engine, + "ARGS", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_args_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* ARGS_COMBINED_SIZE */ + msre_engine_variable_register(engine, + "ARGS_COMBINED_SIZE", + VAR_LIST, + 0, 0, + NULL, + var_args_combined_size_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* ARGS_NAMES */ + msre_engine_variable_register(engine, + "ARGS_NAMES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_args_names_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* ENV */ + msre_engine_variable_register(engine, + "ENV", + VAR_LIST, + 0, 1, + var_env_validate, + var_env_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQBODY_PROCESSOR */ + msre_engine_variable_register(engine, + "REQBODY_PROCESSOR", + VAR_SIMPLE, + 0, 0, + NULL, + var_reqbody_processor_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQBODY_PROCESSOR_ERROR */ + msre_engine_variable_register(engine, + "REQBODY_PROCESSOR_ERROR", + VAR_SIMPLE, + 0, 0, + NULL, + var_reqbody_processor_error_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_BODY + ); + + /* REQBODY_PROCESSOR_ERROR_MSG */ + msre_engine_variable_register(engine, + "REQBODY_PROCESSOR_ERROR_MSG", + VAR_SIMPLE, + 0, 0, + NULL, + var_reqbody_processor_error_msg_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_BODY + ); + + #ifdef WITH_LIBXML2 + /* XML */ + msre_engine_variable_register(engine, + "XML", + VAR_LIST, + 0, 1, + var_xml_validate, + var_xml_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + #endif + + /* WEBSERVER_ERROR_LOG */ + msre_engine_variable_register(engine, + "WEBSERVER_ERROR_LOG", + VAR_LIST, + 0, 0, + NULL, + var_webserver_error_log_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REMOTE_ADDR */ + msre_engine_variable_register(engine, + "REMOTE_ADDR", + VAR_SIMPLE, + 0, 0, + NULL, + var_remote_addr_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REMOTE_HOST */ + msre_engine_variable_register(engine, + "REMOTE_HOST", + VAR_SIMPLE, + 0, 0, + NULL, + var_remote_host_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* REMOTE_PORT */ + msre_engine_variable_register(engine, + "REMOTE_PORT", + VAR_SIMPLE, + 0, 0, + NULL, + var_remote_port_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* REMOTE_USER */ + msre_engine_variable_register(engine, + "REMOTE_USER", + VAR_SIMPLE, + 0, 0, + NULL, + var_remote_user_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* TX */ + msre_engine_variable_register(engine, + "TX", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_tx_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* IP */ + msre_engine_variable_register(engine, + "IP", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_ip_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* SESSION */ + msre_engine_variable_register(engine, + "SESSION", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_session_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* USER */ + msre_engine_variable_register(engine, + "USER", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_user_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* GLOBAL */ + msre_engine_variable_register(engine, + "GLOBAL", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_global_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* RESOURCE */ + msre_engine_variable_register(engine, + "RESOURCE", + VAR_LIST, + 1, 1, + var_generic_list_validate, + var_resource_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* FILES */ + msre_engine_variable_register(engine, + "FILES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_files_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* FILES_COMBINED_SIZE */ + msre_engine_variable_register(engine, + "FILES_COMBINED_SIZE", + VAR_LIST, + 0, 0, + NULL, + var_files_combined_size_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* FILES_NAMES */ + msre_engine_variable_register(engine, + "FILES_NAMES", + VAR_LIST, + 0, 0, + NULL, + var_files_names_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* FILES_SIZES */ + msre_engine_variable_register(engine, + "FILES_SIZES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_files_sizes_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* FILES_TMPNAMES */ + msre_engine_variable_register(engine, + "FILES_TMPNAMES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_files_tmpnames_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* TIME */ + msre_engine_variable_register(engine, + "TIME", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_DAY */ + msre_engine_variable_register(engine, + "TIME_DAY", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_day_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_EPOCH */ + msre_engine_variable_register(engine, + "TIME_EPOCH", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_epoch_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_HOUR */ + msre_engine_variable_register(engine, + "TIME_HOUR", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_hour_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_MIN */ + msre_engine_variable_register(engine, + "TIME_MIN", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_min_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_MON */ + msre_engine_variable_register(engine, + "TIME_MON", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_mon_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_SEC */ + msre_engine_variable_register(engine, + "TIME_SEC", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_sec_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_WDAY */ + msre_engine_variable_register(engine, + "TIME_WDAY", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_wday_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* TIME_YEAR */ + msre_engine_variable_register(engine, + "TIME_YEAR", + VAR_SIMPLE, + 0, 0, + NULL, + var_time_year_generate, + VAR_DONT_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* QUERY_STRING */ + msre_engine_variable_register(engine, + "QUERY_STRING", + VAR_SIMPLE, + 0, 0, + NULL, + var_query_string_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_BASENAME */ + msre_engine_variable_register(engine, + "REQUEST_BASENAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_basename_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_BODY */ + msre_engine_variable_register(engine, + "REQUEST_BODY", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_body_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* REQUEST_COOKIES */ + msre_engine_variable_register(engine, + "REQUEST_COOKIES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_request_cookies_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_COOKIES_NAMES */ + msre_engine_variable_register(engine, + "REQUEST_COOKIES_NAMES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_request_cookies_names_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_FILENAME */ + msre_engine_variable_register(engine, + "REQUEST_FILENAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_filename_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_HEADERS */ + msre_engine_variable_register(engine, + "REQUEST_HEADERS", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_request_headers_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_HEADERS_NAMES */ + msre_engine_variable_register(engine, + "REQUEST_HEADERS_NAMES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_request_headers_names_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_LINE */ + msre_engine_variable_register(engine, + "REQUEST_LINE", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_line_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_METHOD */ + msre_engine_variable_register(engine, + "REQUEST_METHOD", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_method_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_PROTOCOL */ + msre_engine_variable_register(engine, + "REQUEST_PROTOCOL", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_protocol_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_URI */ + msre_engine_variable_register(engine, + "REQUEST_URI", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_uri_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* REQUEST_URI_RAW */ + msre_engine_variable_register(engine, + "REQUEST_URI_RAW", + VAR_SIMPLE, + 0, 0, + NULL, + var_request_uri_raw_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* SERVER_ADDR */ + msre_engine_variable_register(engine, + "SERVER_ADDR", + VAR_SIMPLE, + 0, 0, + NULL, + var_server_addr_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* SERVER_NAME */ + msre_engine_variable_register(engine, + "SERVER_NAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_server_name_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* SERVER_PORT */ + msre_engine_variable_register(engine, + "SERVER_PORT", + VAR_SIMPLE, + 0, 0, + NULL, + var_server_port_generate, + VAR_CACHE, + PHASE_REQUEST_HEADERS + ); + + /* SCRIPT_GID */ + msre_engine_variable_register(engine, + "SCRIPT_GID", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_gid_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_BASENAME */ + msre_engine_variable_register(engine, + "SCRIPT_BASENAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_basename_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_FILENAME */ + msre_engine_variable_register(engine, + "SCRIPT_FILENAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_filename_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_GROUPNAME */ + msre_engine_variable_register(engine, + "SCRIPT_GROUPNAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_groupname_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_MODE */ + msre_engine_variable_register(engine, + "SCRIPT_MODE", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_mode_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_UID */ + msre_engine_variable_register(engine, + "SCRIPT_UID", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_uid_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* SCRIPT_USERNAME */ + msre_engine_variable_register(engine, + "SCRIPT_USERNAME", + VAR_SIMPLE, + 0, 0, + NULL, + var_script_username_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* PATH_INFO */ + msre_engine_variable_register(engine, + "PATH_INFO", + VAR_SIMPLE, + 0, 0, + NULL, + var_path_info_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* AUTH_TYPE */ + msre_engine_variable_register(engine, + "AUTH_TYPE", + VAR_SIMPLE, + 0, 0, + NULL, + var_auth_type_generate, + VAR_CACHE, + PHASE_REQUEST_BODY + ); + + /* RESPONSE_BODY */ + msre_engine_variable_register(engine, + "RESPONSE_BODY", + VAR_SIMPLE, + 0, 0, + NULL, + var_response_body_generate, + VAR_CACHE, + PHASE_RESPONSE_BODY + ); + + /* RESPONSE_HEADERS */ + msre_engine_variable_register(engine, + "RESPONSE_HEADERS", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_response_headers_generate, + VAR_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* RESPONSE_HEADERS_NAMES */ + msre_engine_variable_register(engine, + "RESPONSE_HEADERS_NAMES", + VAR_LIST, + 0, 1, + var_generic_list_validate, + var_response_headers_names_generate, + VAR_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* STATUS_LINE */ + msre_engine_variable_register(engine, + "STATUS_LINE", + VAR_SIMPLE, + 0, 0, + NULL, + var_status_line_generate, + VAR_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* RESPONSE_PROTOCOL */ + msre_engine_variable_register(engine, + "RESPONSE_PROTOCOL", + VAR_SIMPLE, + 0, 0, + NULL, + var_response_protocol_generate, + VAR_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* RESPONSE_STATUS */ + msre_engine_variable_register(engine, + "RESPONSE_STATUS", + VAR_SIMPLE, + 0, 0, + NULL, + var_response_status_generate, + VAR_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* RULE */ + msre_engine_variable_register(engine, + "RULE", + VAR_LIST, + 1, 1, + NULL, + var_rule_generate, + VAR_DONT_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* USERID */ + msre_engine_variable_register(engine, + "USERID", + VAR_SIMPLE, + 0, 0, + NULL, + var_userid_generate, + VAR_DONT_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* SESSIONID */ + msre_engine_variable_register(engine, + "SESSIONID", + VAR_SIMPLE, + 0, 0, + NULL, + var_sessionid_generate, + VAR_DONT_CACHE, + PHASE_RESPONSE_HEADERS + ); + + /* WEBAPPID */ + msre_engine_variable_register(engine, + "WEBAPPID", + VAR_SIMPLE, + 0, 0, + NULL, + var_webappid_generate, + VAR_DONT_CACHE, + PHASE_RESPONSE_HEADERS + ); +} diff --git a/doc/apache_request_cycle-modsecurity.jpg b/doc/apache_request_cycle-modsecurity.jpg new file mode 100644 index 0000000000000000000000000000000000000000..390c649350e15d4a359ca2be8ca15b4a963cc96b GIT binary patch literal 92271 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�wdc= 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%0ANW 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 literal 0 HcmV?d00001 diff --git a/doc/breach-logo-small.gif b/doc/breach-logo-small.gif new file mode 100644 index 0000000000000000000000000000000000000000..d732ac5018a0110c7a1bc11349528f3e1bb966c3 GIT binary patch literal 2389 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 H2?zi?XVITh literal 0 HcmV?d00001 diff --git a/doc/html-chunked.xsl b/doc/html-chunked.xsl new file mode 100644 index 00000000..5158fbca --- /dev/null +++ b/doc/html-chunked.xsl @@ -0,0 +1,292 @@ + + + + + + + +modsecurity-reference.css + + +
ModSecurity
+
+ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/doc/html.xsl b/doc/html.xsl new file mode 100644 index 00000000..5c774adc --- /dev/null +++ b/doc/html.xsl @@ -0,0 +1,20 @@ + + + + + + +
ModSecurity
+
+ + + + + + +
+
+ +modsecurity-reference.css + +
\ No newline at end of file diff --git a/doc/index.html b/doc/index.html new file mode 100644 index 00000000..85c74eed --- /dev/null +++ b/doc/index.html @@ -0,0 +1,35 @@ + + + + ModSecurity for Apache Reference + + + + +
+
ModSecurity
+
+ +
+ +
+ +

ModSecurity for Apache Documentation

+ +

Version $version

+ + + +
+ + + + + diff --git a/doc/modsecurity-logo.png b/doc/modsecurity-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..7fa98c5a3723a4363e1f258c516232f0bac7331b GIT binary patch literal 100823 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

>}ofgy8v0iF13@QK~9x_+MCw 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

FD6}$iU!?hP0!G z>Eo{r5~C*Y>(vz%*)rZvx~^NR-%;%hz?;#n=7mshKwI!PX&x6MyeZ31BQONAoWl)v(9>o)QacxU-5Ec4aF@aBRv^C#aR?w^HVn7v6&YuWccX}QfSN5 zUsh)Gm;do7@?DgB#f>MvD+f(N+B?q(QE}1(LPg?RjOzrxS3;-8b(|9NxRh2B7W{I_ zIDAf1znYE4f4D<#j=Sa3t^1h7$}bYOr(DSzIP(nZ({N+3s+uw~Dq{WbH}T)4y~j%A z72?i6rcpRJJ7-yq38v36g0B5NO0~h(;#2SPjTi44gsiaj2{xu8kt%gxsqVW;Hy(t= zllxD&zy$8?i!6hwBZ-Je7b{$IV8{Yh+6U7+Vfv6sRJ(X2_J*YUtK@rO1#ZkZcVC|LkVM&H8LE* z2;cc$YS(kINVZmbTJZs7YcK7CDlxBmr40`7d)4SsL9ojPo62QMh@BX}P z&3D=4BY{^KU2si0C=b}>udio2ZrB3YokDV4E+;2v^6@!a4`|GQs%z?SK>C8g{4I?0 z25Z=kLOF_TaiTilf472U^wjZ z%@Xv|(n4BN(j!B|;a|Ug{rvOz!2_5zdz@@IxWgtd^f7|3*9ul}=keqC@YHZzLeL%u zF?pc??AH9;(qqom;n@r2npM-S^4~Y62=@uf#4zhieWO4)3`8GaU=t|fd~aa|$e3mF z#B5otk;EjXCMIAInwU5;BJ}GcD=TT~atJg%=rYxXnD+l3pe$tdpO+XxwIli;{Z`5U hp2Gb9*}2q21h(TVPk3g2M-YI&dw2Ejl-z=a{U2sFX0HGM literal 0 HcmV?d00001 diff --git a/doc/modsecurity-reference.css b/doc/modsecurity-reference.css new file mode 100644 index 00000000..1c89c88d --- /dev/null +++ b/doc/modsecurity-reference.css @@ -0,0 +1,102 @@ + +body { + font: 13px/20px Arial, Helvetica; + background-color: white; + width: 800px; +} + +/* TEXT */ + +PRE { + font-size: 100%; + padding: 5px; + border-style: solid; + border-width: 1px; + border-color: #CCCCCC; + background-color: #F5F8FA; +} + +p, td, tr, li, ol, ul { + /*font-family: Trebuchet MS, Verdana, Tahoma, Arial, Helvetica, Geneva;*/ + /*font-family: Arial, Helvetica; + font-size: 14px; + line-height: 24px;*/ +} + +.copyright { + font-size: 10px; +} + +h1 { + padding-top: 40px; + font: 24px/30px Arial, Helvetica; + font-weight: bold; +} + +h2 { + font: 20px/30px Arial, Helvetica; + font-weight: bold; +} + +h3 { + padding-top: 15px; + font: 16px/10px Arial, Helvetica; + font-weight: bold; +} + +h4 { + padding-top: 15px; + font: 14px/10px Arial, Helvetica; + font-weight: bold; +} + + + + +.header { + background-color: #00448B; + border-top: 6px solid #002B6E; + height: 84px; + vertical-align: top; + padding-left: 20px; + padding-top: 10px; +} + +.topNavigation { + background-color: #EEEEEE; + background: url('g/topnav-background.gif'); + height: 23px; + line-height: 12px; + vertical-align: top; + padding-left: 12px; + padding-top: 5px; +} + +.topLink, A.topLink:link, A.topLink:active, A.topLink:visited { + font-weight: bold; + color: black; + text-decoration: none; + padding-left: 8px; + padding-right: 8px; +} + +A.topLink:hover { + color: #BB0000; +} + +#navheader td { + font-size: 12px; +} + +#navfooter td { + font-size: 12px; +} + +h3.title { + margin-top: 0px; +} + +.note { + border-top: 1px solid #CCCCCC; + border-bottom: 1px solid #CCCCCC; +} diff --git a/doc/modsecurity.gif b/doc/modsecurity.gif new file mode 100644 index 0000000000000000000000000000000000000000..76f5ca1632677cb890e0a5ad57cf47cb8a4a6c8d GIT binary patch literal 2585 zcmV+!3g-1kNk%w1VR!%}0OkMyMS8T;+UNWG{a1IVdy~1WyVD0Zeqx@~4m^P!N{eWu z+9FMj7(|8^LWB}Nfh<&!G+UL8qQhB|$|qKpQi;NvtjcVu-t+bNJ#npVhq6Rxo$c`T zLUys-;_OO*x;kyDV4KktK!Rze+>)imRgK2z?DH^Tp62TExy0F|w9hJ9nIlkm!FW4o`jp2nVgKB zq^Y5Xm8+(xjgO_8v$Kh)u)Ds!qmsX!s<67EtCPvKoXpL&uf)R3$-S!6vX{oYmAI#* z&bZvK!l91m;Mvx|+RCY{*wxL%^V;^HuJyjove%iC0NcSUc8j1fWRs8y)Fn?|EL+tW z4ntRt*D!AL^tB3x1c66$qb#ngn5&qk227Z6`E*ekvt!2^vTS&z$%qjI^!bBn5Z|;- z45V=2aAe9Elk4cUqIK?8J5w7SAt=zN&z?V1o<4e6GS|$dhZ?l7V1WXcK~oxJZF-L& zz@7g8;{HOAqG|xF0zF+z2ba^)jDH$7HbWKmr3Zf}n#4JaCv9CkUjWWd{Wapd}0_XnBVLwf5n}4pdZyf&l|)nZplS_OJpA zI)DLy76+7}02FG(p@0}lz%fG^B4FSE8ZU$a1_cy=A%+fJ*l++CWI*8n6k&Lp2OK)Q z0Kym|P(a5QUwF`k9tU`;M;tGRF@qOOupx%PUX+-PDXmsD1Ox_9)rS>+AmC;HA3H#x z>5;^42r&j22l&9i1Uv|_0tO0@K|u%(*zkfG2joEk z8473-0uvb6@d5=CoQw3jOE(Qh3@SkGLDeC6k#!g*gh0U;I(&ct2Sb0r#80fcsVGyWC~JF93lH*jZiK_?N@~v0)BBTL7|1@P{*m1QZHn;M*W5K`T}80diQL207?K zophCkSl2@Zgt4oe3w9|BQ-L&VzA zcz_r^d_fN;c!2`o&<2orBZ|RuAQk6U1iNAJ0T-ys04h)wEYL>(O~&y<0unFGMj7EqvKT2Me3D6oa@MZpgakbxKu z0D={?U<^6{0OUB(Ckdcomoh*A6v)6seEm?E`zvO;LePdII4uTdh=C1oV34LrbBe^P z$_^Hg$vzNa3Y^px7b@9@Av^&BRX7m5J4Ht0D>@#Aq+fdT@)0sh8V;k3cFgC5GDNExmM8iPPU21-zZ008d> zti?hD`$hj>cRl zsK&T_h{3^PKmiqS0S7YB!@)@KfpX3OjUWI49L~Z80Hn7DHS|IYK;YOFgaBURLj!c4 z0S)N7;JRz*v#P0yOXh4J-fwC6dGc1D>#iEck)IeozAgR-ghlCUw1InS3@}1?F@-E- zfub`svUP5QgDhC*5I8^rZYj!%Ea3JGMaIDa?bsa>se*=F0HP$m)14(V0?bE24wJBq z97M3WL;)rhauOh39e6-hjmZZBF~SR|xMLX<`3GR=F%f9``67Pc!UQ~}0Nsg1BogUM zL}t>HL1!c&0oeyMD9wpBe82+goWM$uLLWW@de621OEd_*7nZDg9GGznBN%W500003 zo6g5V9s-PhoI=pO21j1d5 zX!p4s=Gcr&{EX?<{6jK)dC0c6{f>>eWHOItb-0yM>~3GD-K-|aUm!y4K6{%X--tCx vo +

+ ModSecurity Reference Manual + + + Version 2.1.0-rc7 / (February 5, 2007) + + + 2004-2007 + + Breach Security, Inc. (http://www.breach.com) + + + +
+ Introduction + + ModSecurityis a web application + firewall (WAF). With over 70% of all attacks now carried out over the web + application level, organisations need every help they can get in making + their systems secure. WAFs are deployed to establish an external security + layer that increases security, detects, and prevents attacks before they + reach web applications. It provides protection from a range of attacks + against web applications and allows for HTTP traffic monitoring and + real-time analysis with little or no changes to existing + infrastructure. + +
+ HTTP Traffic Logging + + Web servers are typically well-equipped to log traffic in a form + useful for marketing analyses, but fall short when it comes to logging + of traffic to web applications. In particular, most are not capable of + logging the request bodies. Your adversaries know this, and that is why + most attacks are now carried out via POST requests, rendering your + systems blind. ModSecurity makes full HTTP transaction logging possible, + allowing complete requests and responses to be logged. Its logging + facilities also allow fine-grained decisions to be made about exactly + what is logged and when, ensure only the relevant data is + recorded. +
+ +
+ Real-Time Monitoring and Attack Detection + + In addition to providing logging facilities, ModSecurity can + monitor the HTTP traffic in real time in order to detect attacks. In + this case ModSecurity operates as a web intrusion detection tool, + allowing you to react to suspicious events that take place at your web + systems. +
+ +
+ Attack Prevention and Just-in-time Patching + + ModSecurity can also act immediately to prevent attacks from + reaching your web applications. There are three commonly used + approaches: + + + + Negative security model. Negative security model monitors + requests for anomalies, unusual behaviour, and common web + application attacks. It keeps anomaly scores for each request, IP + addresses, application sessions, and user accounts. Requests with + high anomaly scores are either logged or rejected altogether. + + + + Positive security model. When positive security model is + deployed, only requests that are known to be valid are accepted, + with everything else rejected. This approach works best with + applications that are heavily used but rarely updated. + + + + Known weaknesses and vulnerabilities. Its rule language makes + ModSecurity an ideal external patching tool. External patching is + all about reducing the window of opportunity. Time needed to patch + application vulnerabilities often runs to weeks in many + organisations. With ModSecurity, applications can be patched from + the outside, without touching the application source code (and even + without any access to it), making your systems secure until a proper + patch is produced. + + +
+ +
+ Flexible Rule Engine + + A flexible rule engine sits in the heart of ModSecurity. It + implements the ModSecurity Rule Language, which is a specialised + programming language designed to work with HTTP transaction data. The + ModSecurity Rule Language was designed to be easy to use, yet flexible: + common operations are simple while complex operations are possible. + Certified ModSecurity Rules, included with subscription to ModSecurity, + contain a comprehensive set of rules that implement general-purpose + hardening, common web application security issues. Heavily commented, + these rules can be used as a learning tool. +
+ +
+ Embedded-mode Deployment + + ModSecurity is an embeddable web application firewall, which means + it can be deployed as part of your existing web server infrastructure + provided your web servers are Apache-based. This deployment method has + certain advantages: + + + + No changes to existing network. It only takes a few minutes to + add ModSecurity to your existing web servers. And because it was + designed to be completely passive by default, you are free to deploy + it incrementally and only use the features you need. It is equally + easy to remove or deactivate it should decide you don't want it any + more. + + + + No single point of failure. Unlike with network-based + deployments, you will not be introducing a new point of failure to + your system. + + + + Implicit load balancing and scaling. Because it works embedded + in web servers, ModSecurity will automatically take advantage of the + additional load balancing and scalability features. You will not + need to think of load balancing and scaling unless your existing + system needs them. + + + + Minimal overhead. Because it works from inside the web server + process there is no overhead for network communication and minimal + overhead in parsing and data exchange. + + + + No problem with encrypted or compressed content. Many IDS + systems have difficulties analysing SSL traffic. This is not a + problem for ModSecurity because it is positioned to work when the + traffic is decrypted and decompressed. + + + + ModSecurity is known to work well on a wide range of operating + systems. Our customers are successfully running it on Linux, Windows, + Solaris, FreeBSD, OpenBSD, NetBSD, AIX, Mac OS X, and HP-UX. +
+ +
+ Network-based Deployment + + ModSecurity works equally well when deployed as part of an + Apache-based reverse proxy server, and many of our customers choose to + do so. In this scenario, one installation of ModSecurity can protect any + number of web servers (even the non-Apache ones). +
+ +
+ Licensing + + ModSecurity is available under two licenses. Users can choose to + use the software under the terms of the GNU General Public License + (http://www.gnu.org/licenses/gpl.html),as + an Open Source / Free Software product. A range of commercial licenses + is also available, together with a range of commercial support + contracts. For more information on commercial licensing please contact + Breach Security. + + + ModSecurity, mod_security, and ModSecurity Pro are trademarks or + registered trademarks of Breach Security. + +
+
+ +
+ ModSecurity Core Rules + +
+ Overview + + ModSecurity is a web application firewall engine that provides + very little protection on its own. In order to become useful ModSecurity + must be configured with rules. In order to enable users to take full + advantage of ModSecurity out of the box, Breach Security Inc. is + providing a free certified rule set for ModSecurity 2.0. Unlike + intrusion detection and prevention systems, which rely on signature + specific to known vulnerabilities, the Core Rules provide generic + protection from unknown vulnerabilities often found in web applications, + which are in most cases custom coded. The Core Rules are heavily + commented to allow it to be used as a step-by-step deployment guide for + ModSecurity. The latest Core Rules can be found at the ModSecurity + website -http://www.modsecurity.org/projects/rules/index.html. +
+ +
+ Core Rules Structure + + If you expect a single pack of Apache configuration files, you are + right, and wrong. A ModSecurity rule set includes information about + different areas: + + + + The logic required to detect attacks. + + + + A policy setting the actions to perform if an attack is + detected. + + + + Information regarding attacks. + + + + In order to allow separate management of the different parts, the + Core Rules are based on templates that are generated into a run-time + rule set by inserting policy, patterns and event information. The Core + Rules package includes these templates, the generation script (written + in Perl) and data files required to generate a useful rule set. It also + includes a bunch of pre-generated rule sets for different policies. The + generation script also allows two optimizations: + + + + Optimal use of regular expressions. Since regular expressions + are much more efficient if assembled into a single expression and + optimized, the generation script takes the list of patterns that are + required for a rule and optimize them into a most efficient regular + expression. + + + + Removal of rules that are not utilized by a specific + policy. + + +
+ +
+ Core Rules Content + + In order to provide generic web applications protection, the Core + Rules use the following techniques: + + + + HTTP protection - detecting violations of the HTTP protocol + and a locally defined usage policy. + + + + Common Web Attacks Protection - detecting common web + application security attack. + + + + Automation detection - Detecting bots, crawlers, scanners and + other surface malicious activity. + + + + Trojan Protection - Detecting access to Trojans horses. + + + + Error Hiding - Disguising error messages sent by the + server. + + +
+
+ +
+ Installation + + ModSecurity installation consists of the following steps: + + + + ModSecurity 2.x works with Apache 2.0.x or better. + + + + Make sure you have mod_unique_idinstalled. + + + + (Optional) Install the latest version of libxml2, if it isn't + already installed on the server. + + + + Unpack the ModSecurity archive + + + + Edit Makefile to configure the path to the Apache ServerRoot + directory. You can check this by identifying the ServerRoot directive + setting in your httpd.conf file. This is the path that was specified + with the "--install-path=" configuration flag during compilation (for + example, in Fedora Core4: top_dir = + /etc/httpd). + + + + (Optional) Edit Makefile to enable ModSecurity to use libxml2 + (uncomment lineDEFS = + -DWITH_LIBXML2) and configure the include path (for example: + INCLUDES=-I/usr/include/libxml2) + + + + Compile with make + + + + Stop Apache + + + + Install with make + install + + + + (Optional) Add one line to your configuration to load + libxml2:LoadFile + /usr/lib/libxml2.so + + + + Add one line to your configuration to load ModSecurity:LoadModule security2_module + modules/mod_security2.so + + + + Configure ModSecurity + + + + Start Apache + + + + You now have ModSecurity 2.x up and running. + + + + + If you have compiled Apache yourself you might experience problems + compiling ModSecurity against PCRE. This is because Apache bundles PCRE + but this library is also typically provided by the operating system. I + would expect most (all) vendor-packaged Apache distributions to be + configured to use an external PCRE library (so this should not be a + problem). + + You want to avoid Apache using the bundled PCRE library and + ModSecurity linking against the one provided by the operating system. + The easiest way to do this is to compile Apache against the PCRE library + provided by the operating system (or you can compile it against the + latest PCRE version you downloaded from the main PCRE distribution + site). You can do this at configure time using the --with-pcre switch. If you are not in a + position to recompile Apache then, to compile ModSecurity successfully, + you'd still need to have access to the bundled PCRE headers (they are + available only in the Apache source code) and change the include path + for ModSecurity (as you did in step 7 above) to point to them. + + Do note that if your Apache is using an external PCRE library you + can compile ModSecurity with WITH_PCRE_STUDY defined,which would possibly + give you a slight performance edge in regular expression + processing. + +
+ +
+ Configuration Directives + + The following section outlines all of the ModSecurity directives. + Most of the ModSecurity directives can be used inside the various Apache + Scope Directives such as Virtual Hosts, Location, LocationMatch, + Directory, etc... There are others, however, that can only be used once in + the main configuration file. This information is specified in the Scope + sections below. + + These rules, along with the Core rules files, should be contained is + files outside of the httpd.conf file and called up with Apache "Include" + directives. This allows for easier updating/migration of the rules. If you + create your own custom rules that you would like to use with the Core + rules, you should create a file called - + modsecurity_crs_15_customrules.conf and place it in the same directory as + the Core rules files. By using this file name, your custom rules will be + called up after the standard ModSecurity Core rules configuration file but + before the other Core rules. This allows your rules to be evaluate first + which can be useful if you need to implement specific "allow" rules or to + correct any false positives in the Core rules as they are applied to your + site. + + Note + + It is highly encouraged that you do not edit the Core rules files + themselves but rather place all changes (such as SecRuleRemoveByID, + etc...) in your custom rules file. This will allow for easier upgrading as + newer Core rules are released by Breach on the ModSecurity website. + +
+ <literal>SecAction</literal> + + Description: Unconditionally + processes the action list it receives as the first and only parameter. + It accepts one parameter, the syntax of which is identical to the third + parameter of SecRule. + + Syntax: SecActionaction 1,action2,action2 + + Example Usage: SecAction + nolog,redirect:http://www.hostname.com + + ProcessingPhase: Any + + Scope: + Any + + Dependencies/Notes: None + + SecAction is best used when you uncondiationally execute an + action. This is explicit triggering whereas the normal Actions are + conditional based on data inspection of the request/response. This is a + useful directive when you want to run certian actions such as initcol to + initialize collections. +
+ +
+ <literal>SecArgumentSeparator</literal> + + Description: Specifies which + character to use as separator for + application/x-www-form-urlencoded content. Defaults to&. Applications are sometimes (very + rarely) written to use a semicolon (;). + + Syntax: SecArgumentSeparator character + + Example Usage: SecArgumentSeparator ; + + Processing Phase: Any + + Scope: + Main + + Dependencies/Notes: None + + This directive is needed if a backend web appliaction is using a + non-standard argument separator. If this directive is not set properly + for each web app, then ModSecurity will not be able to parse the + arguements appropriately and the effectiveness of the rule matching will + be significantly decreased. +
+ +
+ <literal>SecAuditEngine</literal> + + Description: Configures the audit + logging engine. + + Syntax: SecAuditEngine On|Off|RelevantOnly + + Example Usage: SecAuditEngine On + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Can be + set/changed with the "ctl" action for the current transaction. + + Example: The following example shows the various audit directives + used together. + + SecAuditEngine RelevantOnly +SecAuditLog logs/audit/audit.log +SecAuditLogParts ABCFHZ +SecAuditLogType concurrent +SecAuditLogStorageDir logs/audit +SecAuditLogRelevantStatus "^(4|5)" + + Possible values are: + + + + On - log all transactions + by default. + + + + Off - do not log + transactions by default. + + + + RelevantOnly - by default + only log transactions that have triggered a warning or an error, or + have a status code that is considered to be relevant (see SecAuditLogRelevantStatus). + + +
+ +
+ <literal>SecAuditLog</literal> + + Description: Defines the path to + the main audit log file. + + Syntax: SecAuditLog /path/to/auditlog + + Example Usage: SecAuditLog + /usr/local/apache/logs/audit.log + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: This file is + open on startup when the server typically still runs as + root. You should not allow non-root users to have write + privileges for this file or for the directory it is stored in.. + + This file will be used to store the audit log entries if serial + audit logging format is used. If concurrent audit logging format is used + this file will be used as an index, and contain a record of all audit + log files created. If you are planning to use Concurrent audit logging + and sending your audit log data off to a remote Console host, then you + will need to use the modsec-auditlog-collector.pl script and use the + following format: + + SecAuditLog "|/path/to/modsec-auditlog-collector.pl + /path/to/SecAuditLogDataDir /path/to/SecAuditLog" +
+ +
+ <literal>SecAuditLogParts</literal> + + Description: Defines the path to + the main audit log file. + + Syntax: SecAuditLogParts PARTS + + Example Usage: SecAuditLogParts ABCFHZ + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: At this time + ModSecurity does not log response bodies of stock Apache responses + (e.g.404), or the Server and Date + response headers. + + DefaultABCFHZ. + + Available audit log parts: + + + + A– audit log header + (mandatory) + + + + B– request headers + + + + C– request body (present + only if the request body exists and ModSecurity is configured to + intercept it) + + + + D- RESERVED for + intermediary response headers, not implemented yet. + + + + E– intermediary response + body (present only if ModSecurity is configured to intercept + response bodies, and if the audit log engine is configured to record + it). Intermediary response body is the same as the actual response + body unless ModSecurity intercepts the intermediary response body, + in which case the actual response body will contain the error + message (either the Apache default error message, or the + ErrorDocument page). + + + + F– final response headers + (excluding the Date and Server headers, which are always added by + Apache in the late stage of content delivery). + + + + G– RESERVED for the actual + response body, not implemented yet. + + + + H- audit log trailer + + + + I- This part is a + replacement for part C. It will log the same data as C in all cases + except whenmultipart/form-dataencoding in used. In + this case it will log a fake + application/x-www-form-urlencoded body that contains the + information about parameters but not about the files. This is handy + if you don't want to have (often large) files stored in your audit + logs. + + + + J- RESERVED. This part, + when implemented, will contain information about the files uploaded + using multipart/form-data encoding. + + + + Z– final boundary, + signifies the end of the entry (mandatory) + + +
+ +
+ <literal>SecAuditLogRelevantStatus</literal> + + Description: Configures which + response status code is to be considered relevant for the purpose of + audit logging. + + Syntax: SecAuditLogRelevantStatus REGEX + + Example Usage: SecAuditLogRelevantStatus ^(4|5) + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Must have the + SecAuditEngine set to RelevantOnly. The parameter is a regular + expression. + + The main purpose of this directive is to allow you to configure + audit logging for only transactions that generate the specified HTTP + Response Status Code. This directive is often used to the decrease the + total size of the audit log file. Keep in mind that if this parameter is + used, then successful attacks that result in a 200 OK status code will + not be logged. +
+ +
+ <literal>SecAuditLogStorageDir</literal> + + Description: Configures the + storage directory where concurrent audit log entries are to be + stored. + + Syntax: SecAuditLogStorageDir + /path/to/storage/dir + + Example Usage: SecAuditLogStorageDir + /usr/local/apache/logs/audit + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: + SecAuditLogType must be set to Concurrent. The directory must already be + created before starting Apache and it must be writable by the web server + user as new files are generated at runtime. + + As with all logging mechanisms, ensure that you specify a file + system location that as adequate disk space and is not on the root + partition. +
+ +
+ <literal>SecAuditLogType</literal> + + Description: Configures the type + of audit logging mechanism to be used. + + Syntax: SecAuditLogType serial|concurrent + + Example Usage: SecAuditLogType serial + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Must specify + SecAuditLogStorageDir if you use concurrent logging. + + Possible values are: + + + + Serial - all audit log + entries will be stored in the main audit logging file. This is more + convenient for casual use but it is slower as only one audit log + entry can be written to the file at any one file. + + + + Concurrent - audit log + entries will be stored in separate files, one for each transaction. + Concurrent logging is the mode to use if you are going to send the + audit log data off to a remote ModSecurity Console host. + + +
+ +
+ <literal>SecChrootDir</literal> + + Description: Configures the + directory path that will be used to jail the web server process. + + Syntax: SecChrootDir /path/to/chroot/dir + + Example Usage: SecChrootDir /chroot + + Processing Phase: N/A + + Scope: + Main + + Dependencies/Notes: The internal + chroot functionality provided by ModSecurity works great for simple + setups. One example of a simple setup is Apache serving static files + only, or running scripts using modules. For more complex setups you + should consider building a jail the old-fashioned way. The internal + chroot feature should be treated as somewhat experimental. Due to the + large number of default and third-party modules available for the Apache + web server, it is not possible to verify the internal chroot works + reliably with all of them. You are advised to think about your option + and make your own decision. In particular, if you are using any of the + modules that fork in the module initialisation phase (e.g. mod_fastcgi, + mod_fcgid, mod_cgid), you are advised to examine each Apache process and + observe its current working directory, process root, and the list of + open files. +
+ +
+ <literal>SecCookieFormat</literal> + + Description: Selects the cookie + format that will be used in the current configuration context. + + Syntax: SecCookieFormat 0|1 + + Example Usage: SecCookieFormat 0 + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: None + + Possible values are: + + + + 0 - use version 0 + (Netscape) cookies. This is what most applications use. It is the + default value. + + + + 1 - use version 1 + cookies. + + +
+ +
+ <literal>SecDataDir</literal> + + Description: Path where + persistent data (e.g. IP address data, session data, etc) is to be + stored. + + Syntax: SecDataDir /path/to/dir + + Example Usage: SecDataDir /usr/local/apache/logs/data + + Processing Phase: N/A + + Scope: + Main + + Dependencies/Notes: This + directive is needed when initcol, setsid an setuid are used. Must be + writable by the web server user. +
+ +
+ <literal>SecDebugLog</literal> + + Description: Path to the + ModSecurity debug log file. + + Syntax: SecDebugLog /path/to/modsec-debug.log + + Example Usage: SecDebugLog + /usr/local/apache/logs/modsec-debug.log + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: None +
+ +
+ <literal>SecDebugLogLevel</literal> + + Description: Configures the + verboseness of the debug log data. + + Syntax: SecDebugLogLevel 0|1|2|3|4|5|6|7|8|9 + + Example Usage: SecDebugLogLevel 4 + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Levels + 1-3 + are always sent to the Apache error log. Therefore you can + always use level 0 as the default + logging level in production. Level 5 + is useful when debugging. It is not advisable to use higher + logging levels in production as excessive logging can slow down server + significantly. + + Possible values are: + + + + 0 - no logging. + + + + 1 - errors (intercepted + requests) only. + + + + 2 - warnings. + + + + 3 - notices. + + + + 4 - details of how + transactions are handled. + + + + 5 - as above, but including + information about each piece of information handled. + + + + 9 - log everything, + including very detailed debugging information. + + +
+ +
+ <literal>SecDefaultAction</literal> + + Description: Defines the default + action to take on a rule match. + + Syntax: SecDefaultAction + action1,action2,action3 + + Example Usage: SecDefaultAction + log,auditlog,deny,status:403,phase:2,t:lowercase + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: Rules + following a SecDefaultAction directive will inherit this setting unless + a specific action is specified for an indivdual rule or until another + SecDefaultAction is specified. + + The default value is: + + SecDefaultAction log,auditlog,deny,status:403,phase:2,t:lowercase,t:replaceNulls,t:compressWhitespace + + Note + + SecDefaultAction must specify a disruptive action and a processing + phase. +
+ +
+ <literal>SecGuardianLog</literal> + + Description: Configuration + directive to use the httpd-guardian script to monitor for Denial of + Service (DoS) attacks. + + Syntax: SecGuardianLog |/path/to/httpd-guardian + + Example Usage: SecGuardianLog + |/usr/local/apache/bin/httpd-guardian + + Processing Phase: N/A + + Scope: + Main + + Dependencies/Notes: By default + httpd-guardian will defend against clients that send more 120 requests + in a minute, or more than 360 requests in five minutes. + + Since 1.9 ModSecurity supports a new directive, SecGuardianLog, + that is designed to send all access data to another program using the + piped logging feature. Since Apache is typically deployed in a + multi-process fashion, making information sharing difficult, the idea is + to deploy a single external process to observe all requests in a + stateful manner, providing additional protection. + + Development of a state of the art external protection tool will be + a focus of subsequent ModSecurity releases. However, a fully functional + tool is already available as part of the Apache httpd tools project + (http://www.apachesecurity.net/tools/). The + tool is called httpd-guardian and can be used to defend against Denial + of Service attacks. It uses the blacklist tool (from the same project) + to interact with an iptables-based (Linux) or pf-based (*BSD) firewall, + dynamically blacklisting the offending IP addresses. It can also + interact with SnortSam (http://www.snortsam.net). Assuming + httpd-guardian is already configured (look into the source code for the + detailed instructions) you only need to add one line to your Apache + configuration to deploy it: + + SecGuardianLog |/path/to/httpd-guardian +
+ +
+ <literal>SecRequestBodyAccess</literal> + + Description: Configures whether + request bodies will be buffered and processed by ModSecurity by + default. + + Syntax: SecRequestBodyAccess On|Off + + Example Usage: SecRequestBodyAccess On + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Thisdirective + is required if you plan to inspect POST_PAYLOADS of requests. This + directive must be used along with the "phase:2" processing phase action + and REQUEST_BODY variable/location. If any of these 3 parts are not + configured, you will not be able to inspect the request bodies. + + Possible values are: + + + + On - access request + bodies. + + + + Off - do not attempt to + access request bodies. + + +
+ +
+ <literal>SecRequestBodyLimit</literal> + + Description: Configures the + maximum request body size ModSecurity will accept for buffering. + + Syntax: SecRequestBodyLimit NUMBER_IN_BYTES + + Example Usage: SecRequestBodyLimit 134217728 + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: 131072 KB + (134217728 bytes) is the default setting. Anything over this limit will + be rejected with status code 413 Request Entity Too Large. There is a + hard limit of 1 GB. +
+ +
+ <literal>SecRequestBodyInMemoryLimit</literal> + + Description: Configures the + maximum request body size ModSecurity will store in memory. + + Syntax: SecRequestBodyInMemoryLimit + NUMBER_IN_BYTES + + Example Usage: SecRequestBodyInMemoryLimit 131072 + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: None + + By default the limit is 128 KB: + + # Store up to 128 KB in memory +SecRequestBodyInMemoryLimit 131072 +
+ +
+ <literal>SecResponseBodyLimit</literal> + + Description: Configures the + maximum response body size that will be accepted for buffering. + + Syntax: SecResponseBodyLimit NUMBER_IN_BYTES + + Example Usage: SecResponseBodyLimit 524228 + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Anything over + this limit will be rejected with status code 500 Internal Server Error. + This setting will not affect the responses with MIME types that are not + marked for buffering. There is a hard limit of 1 GB. + + By default this limit is configured to 512 KB: + + # Buffer response bodies of up to 512 KB in length +SecResponseBodyLimit 524288 +
+ +
+ <literal>SecResponseBodyMimeType</literal> + + Description: Configures + which MIME types are to be considered + for response body buffering. + + Syntax: SecResponseBodyMimeType mime/type + + Example Usage: SecResponseBodyMimeType text/plain + text/html + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: + Multiple SecResponseBodyMimeType + directives can be used to add MIME + types. + + The default value is text/plaintext/html: + + SecResponseBodyMimeType text/plain text/html +
+ +
+ <literal>SecResponseBodyMimeTypesClear</literal> + + Description: Clears the list of + MIME types considered for response + body buffering, allowing you to start populating the list from + scratch. + + Syntax: SecResponseBodyMimeTypesClear + + Example Usage: SecResponseBodyMimeTypesClear + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: None +
+ +
+ <literal>SecResponseBodyAccess</literal> + + Description: Configures whether + response bodies are to be buffer and analysed or not. + + Syntax: SecResponseBodyAccess On|Off + + Example Usage: SecResponseBodyAccess On + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: This + directive is required if you plan to inspect html responses. This + directive must be used along with the "phase:4" processing phase action + and RESPONSE_BODY variable/location. If any of these 3 parts are not + configured, you will not be able to inspect the response bodies. + + Possible values are: + + + + On - access response bodies + (but only if the MIME type matches, see above). + + + + Off - do not attempt to + access response bodies. + + +
+ +
+ <literal>SecRule</literal> + + Description: SecRuleis the main ModSecurity directive. It + is used to analyse data and perform actions based on the results. + + Syntax: SecRuleVARIABLES OPERATOR [ACTIONS] + + Example Usage: SecRuleREQUEST_URI "attack" + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: None + + In general, the format of this rule is as follows: + + SecRule VARIABLES OPERATOR [ACTIONS] + + The second part, OPERATOR, + specifies how they are going to be checked. The third (optional) part, + ACTIONS, specifies what to do + whenever the operator used performs a successful match against a + variable. + +
+ Variables in rules + + The first part, VARIABLES, + specifies which variables are to be checked. For example, the + following rule will reject a transaction that has the word + dirty in the URI: + + SecRule REQUEST_URI dirty + + Each rule can specify one or more variables: + + SecRule REQUEST_URI|QUERY_STRING dirty + + There is a third format supported by the selection operator - + XPath expression. XPath expressions can only used against the special + variable XML, which is available only of the request body was + processed as XML. + + SecRule XML:/xPath/Expression dirty + + + As you have just seen, not all collections support all + selection operator format types. You should refer to the + documentation of each collection to determine what is and isn't + supported. + +
+ +
+ Operators in rules + + In the simplest possible case you will use a regular expression + pattern as the second rule parameter. This is what we've done in the + examples above. If you do this ModSecurity assumes you want to use + the rx operator. You can explicitly + specify the operator you want to use by using @ as the first character in the second rule + parameter: + + SecRule REQUEST_URI "@rx dirty" + + Note how we had to use double quotes to delimit the second rule + parameter. This is because the second parameter now has a whitespace + in it. Any number of whitespace characters can follow the name of the + operator. If there are any non-whitespace characters there, they will + all be treated as a special parameter to the operator. In the case of + the regular expression operator the special parameter is the pattern + that will be used for comparison. + + The @ can be the second character if you are using negation to + negate the result returned by the operator: + + SecRule &ARGS "!@rx ^0$" +
+ +
+ Actions in rules + + The third parameter, ACTIONS, + can be omitted only because there is a helper feature that specifies + the default action list. If the parameter isn't omitted the actions + specified in the parameter will be merged with the default action list + to create the actual list of actions that will be processed on a rule + match. +
+
+ +
+ <literal>SecRuleInheritance</literal> + + Description: Configures whether + the current context will inherit rules from the parent context + (configuration options are inherited in most cases - you should look up + the documentation for every directive to determine if it is inherited or + not). + + Syntax: SecRuleInheritance On|Off + + Example Usage: SecRuleInheritance Off + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: + Resource-specific contexts (e.g. + Location, Directory, etc) + cannot overridephase1rules configured in the main + server or in the virtual server. This is because phase 1 is run early in + the request processing process, before Apache maps request to resource. + Virtual host context can override phase 1 rules configured in the main + server. + + Example: The following example shows where ModSecurity may be + enabled in the main Apache configuration scope, however you might want + to configure your VirtualHosts differently. In the first example, the + first virtualhost is not inheriting the ModSecurity main config + directives and in the second one it is. + + SecRuleEnine On +SecDefaultAction log,pass,phase:2 +... + +<VirtualHost *:80> +ServerName app1.com +ServerAlias www.app1.com +SecRuleInheritance Off +SecDefaultAction log,deny,phase:1,redirect:http://www.site2.com +... +</VirtualHost> + +<VirtualHost *:80> +ServerName app2.com +ServerAlias www.app2.com +SecRuleInheritance On +SecRule ARGS "attack" +... +</VirtualHost> + + Possible values are: + + + + On - inherit rules from the + parent context. + + + + Off - do not inherit rules + from the parent context. + + +
+ +
+ <literal>SecRuleEngine</literal> + + Description: Configures the rules + engine. + + Syntax: SecRuleEngineOn|Off|DetectionOnly + + Example Usage: SecRuleEngine On + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: Thisdirective + can also be controled by the ctl action (ctl:ruleEngine=off) for per + rule processing. + + Possible values are: + + + + On - process rules. + + + + Off - do not process + rules. + + + + DetectionOnly - process + rules but never intercept transactions, even when rules are + configured to do so. + + +
+ +
+ <literal>SecRuleRemoveById</literal> + + Description: Removes matching + rules from the parent contexts. + + Syntax: SecRuleRemoveById RULEID + + Example Usage: SecRuleRemoveByID 1 2 "9000-9010" + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: This + directive supports multiple parameters, where each parameter can either + be a rule ID, or a range. Parameters that contain spaces must be + delimited using double quotes. + + SecRuleRemoveById 1 2 5 10-20 "400-556" 673 +
+ +
+ <literal>SecRuleRemoveByMsg</literal> + + Description: Removes matching + rules from the parent contexts. + + Syntax: SecRuleRemoveByMsg REGEX + + Example Usage: SecRuleRemoveByMsg "FAIL" + + Processing Phase: Any + + Scope: + Any + + Dependencies/Notes: This + directive supports multiple parameters. Each parameter is a regular + expression that will be applied to the message (specified using the + msg action). +
+ +
+ <literal>SecServerSignature</literal> + + Description: Instructs + ModSecurity to change the data presented in the "Server:" response + header token. + + Syntax: SecServerSignature "WEB SERVER + SOFTWARE" + + Example Usage: SecServerSignature + "Netscape-Enterprise/6.0" + + Processing Phase: N/A + + Scope: + Main + + Dependencies/Notes: In order for + this directive to work, you must set the Apache ServerTokens directive + to Full. ModSecurity will overwrite the server signature data held in + this memory space with the data set in this directive. If ServerTokens + is not set to Full, then the memory space is most likely not large + enough to hold the new data we are looking to insert. +
+ +
+ <literal>SecTmpDir</literal> + + Description: Configures the + directory where temporary files will be created. + + Syntax: SecTmpDir /path/to/dir + + Example Usage: SecTmpDir /tmp + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: Needs to be + writable by the Apache user process. This is the directory location + where Apache will swap data to disk if it runs out of memory (more data + than what was specified in the SecRequestBodyInMemoryLimit directive) + during inspection. +
+ +
+ <literal>SecUploadDir</literal> + + Description: Configures the + directory where intercepted files will be stored. + + Syntax: SecUploadDir /path/to/dir + + Example Usage: SecUploadDir /tmp + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: This + directory must be on the same filesystem as the temporary directory + defined with SecTmpDir. This + directive is used with SecUploadKeepFiles. +
+ +
+ <literal>SecUploadKeepFiles</literal> + + Description: Configures whether + or not the intercepted files will be kept after transaction is + processed. + + Syntax: SecUploadKeepFiles On|Off|RelevantOnly + + Example Usage: SecUploadKeepFiles On + + Processing Phase: N/A + + Scope: + Any + + Dependencies/Notes: This + directive requires the storage directory to be defined (using SecUploadDir). + + Possible values are: + + + + On - Keep uploaded + files. + + + + Off - Do not keep uploaded + files. + + + + RelevantOnly - This will + keep only those files that belong to requests that are deemed + relevant. + + +
+ +
+ <literal>SecWebAppId</literal> + + Description: Creates a partition + on the server that belongs to one web application. + + Syntax: SecWebAppId "NAME" + + Example Usage: SecWebAppId "WebApp1" + + Processing Phase:N/A + + Scope: + Any + + Dependencies/Notes: Partitions + are used to avoid collisions between session IDs and user IDs. This + directive must be used if there are multiple applications deployed on + the same server. If it isn't a collision between session IDs might + occur. The default value is default. + Example: + + <VirtualHost *:80> +ServerName app1.com +ServerAlias www.app1.com +SecWebAppId "App1" +SecRule REQUEST_COOKIES:PHPSESSID !^$ chain,nolog,pass +SecAction setsid:%{REQUEST_COOKIES.PHPSESSID} +... +</VirtualHost> + +<VirtualHost *:80> +ServerName app2.com +ServerAlias www.app2.com +SecWebAppId "App2" +SecRule REQUEST_COOKIES:PHPSESSID !^$ chain,nolog,pass +SecAction setsid:%{REQUEST_COOKIES.PHPSESSID} +... +</VirtualHost> + + In the two examples configurations shown, SecWebAppId is being + used in conjuction with the Apache VirtualHost directives. What this + achieves is to create more unique collection names when being hosted on + one server. Normally, when setsid is used, ModSecurity will create a + collection with the name "SESSION" and it will hold the value specified. + With using SecWebAppId as shown in the examples, however, the name of + the collection would become "App1_SESSION" and "App2_SESSION". + + SecWebAppId is relevant in two cases: + + + + You are logging transactions/alerts to the ModSecurity Console + and you want to use the web application ID to search only the + transactions belonging to that application. + + + + You are using the data persistence facility (collections + SESSION and USER) and you need to avoid collisions between sessions + and users belonging to different applications. + + +
+
+ +
+ Processing Phases + + ModSecurity 2.x allows rules to be placed in one of the following + five phases: + + + + Request headers + + + + Request body + + + + Response headers + + + + Response body + + + + Logging + + + + ModSecurity Processing Phases + Diagram + + Below is a diagram of the standard Apache + Request Cycle. In the diagram, the 5 ModSecurity processing phases are + shown. + + + + In order to select the phase a rule executes during, use the phase + action either directly in the rule or in using the + SecDefaultAction directive: + + SecDefaultAction "log,pass,phase:2" +SecRule HTTP_Host "!^$" "deny,phase:1" + + Note on Rule and Phases + + Keep in mind that rules are executed according to phases, so even if + two rules are adjacent in a configuration file, but are set to execute in + different phases, they would not happen one after the other. The order of + rules in the configuration file is important only within the rules of each + phase. This is especially important when using the skip + action. + +
+ Phase Request Headers + + Rules in this phase immediately after Apache completes reading the + request headers (post-read-request phase). At this point the request + body has not been read yet, meaning not all request arguments are + available. Rules should be placed in this phase if you need to have them + run early (before Apache does something with the request), to do + something before the request body has been read, determine whether or + not the request body should be buffered, or decide how you want the + request body to be processed (e.g. whether to parse it as XML or + not). + + Note + + Rules in this phase can not leverage Apache scope directives + (Directory, Location, LocationMatch, etc...) as the post-read-request + hook does not have this information yet. The exception here is the + VirtualHost directive. If you want to use ModSecurity rules inside + Apache locations, then they should run in Phase 2. Refer to the Apache + Request Cycle/ModSecurity Processing Phases diagram. +
+ +
+ Phase Request Body + + This is the general-purpose input analysis phase. Most of the + application-oriented rules should go here. In this phase you are + guaranteed to have received the request argument (provided the request + body has been read). ModSecurity supports three encoding types for the + request body phase: + + + + application/x-www-form-urlencoded - used to transfer form + data + + + + multipart/form-data – used for file transfers + + + + text/xml - used for passing XML data + + + + Other encodings are not used by most web applications. +
+ +
+ Phase Response Headers + + This phase takes place just before response headers are sent back + to the client. Run here if you want to observe the response before that + happens, and if you want to use the response headers to determine if you + want to buffer the response body. Note that some response status codes + (such as 404) are handled earlier in the request cycle by Apache and my + not be able to be triggered as expected. Additionally, there are some + response headers that are added by Apache at a later hook (such as Date, + Server and Connection) that we would not be able to trigger on or + sanitize. This should work appropirately in a proxy setup + however. +
+ +
+ Phase Response Body + + This is the general-purpose output analysis phase. At this point + you can run rules against the response body (provided it was buffered, + of course). This is the phase where you would want to inspect the + outbound html for information discloure, error messages or failed + authentication text. +
+ +
+ Phase Logging + + This phase is run just before logging takes place. The rules + placed into this phase can only affect how the logging is performed. + This phase can be used to inspect the error messages logged by Apache. + You can not deny/block connections in this phase as it is too + late. +
+
+ +
+ Variables + + The following variables are supported in ModSecurity 2.x: + +
+ <literal moreinfo="none">ARGS</literal> + + ARGS is a collection and can be used on its own + (means all arguments including the POST Payload), with a static + parameter (matches arguments with that name), or with a regular + expression (matches all arguments with name that matches the regular + expression). Note: ARGS:p will not result in any + invocations against the operator if argument p does not exist. Some + variables are actually collections, which are expanded into more + variables at runtime. The following example will examine all request + arguments:SecRule ARGS dirtySometimes, + however, you will want to look only at parts of a collection. This can + be achieved with the help of theselection + operator(colon). The following example will only look at the + arguments named p (do note that, in + general, requests can contain multiple arguments with the same name): + SecRule ARGS:p dirtyIt + is also possible to specify exclusions. The following will examine all + request arguments for the word dirty, except the + ones named z (again, there can be + zero or more arguments named z): + SecRule ARGS|!ARGS:z dirtyThere + is a special operator that allows you to count how many variables there + are in a collection. The following rule will trigger if there is more + than zero arguments in the request (ignore the second parameter for the + time being): SecRule &ARGS !^0$And + sometimes you need to look at an array of parameters, each with a + slightly different name. In this case you can specify a regular + expression in the selection operator itself. The following rule will + look into all arguments whose names begin with id_: SecRule ARGS:/^id_/ dirty + Note + In ModSecurity 1.X, the ARGS variable stood + for QUERY_STRING + POST_PAYLOAD, + whereas now it expands to to individual variables. +
+ +
+ <literal moreinfo="none">ARGS_COMBINED_SIZE</literal> + + This variable allows you to set more targeted evaluations on the + total size of the Arguments as compared with normal Apache LimitRequest + directives. For example, you could create a rule to ensure that the + total size of the argument data is below a certain threshold (to help + prevent buffer overflow issues). Example: Block request if the size of + the arguments is above 25 characters. + + SecRule REQUEST_FILENAME "^/cgi-bin/login\.php$" "chain,log,deny,status:403,phase:2" +SecRule ARGS_COMBINED_SIZE "@gt 25" +
+ +
+ <literal moreinfo="none">ARGS_NAMES</literal> + + Is a collection of the argument names. You can search for specific + argument names that you want to block. In a positive policy scenario, + you can also whitelist (using an inverted rule with the ! character) + only authorized argument names. Example: This example rule will only + allow 2 argument names - p and a. If any other argument names are + injected, it will be blocked. + + SecRule REQUEST_FILENAME "/index.php" "chain,log,deny,status:403,phase:2" +SecRule ARGS_NAMES "!^(p|a)$" +
+ +
+ <literal moreinfo="none">AUTH_TYPE</literal> + + This variable holds the authentication method used to validate a + user. Example: + + SecRule AUTH_TYPE "basic" log,deny,status:403,phase:1,t:lowercase + + Note + + This data will not be available in a proxy-mode deployment as the + authentication is not local. In a proxy-mode deployment, you would need + to inpect the REQUEST_HEADERS:Authorization + header. +
+ +
+ <literal moreinfo="none">ENV</literal> + + Collection, requires a single parameter (after a colon character). + The ENV variable is set with setenv and does not give access to the CGI + environment variables. Example: + + SecRule REQUEST_FILENAME "printenv" pass,setenv:tag=suspicious +SecRule ENV:tag "suspicious" +
+ +
+ <literal moreinfo="none">FILES</literal> + + Collection. Contains a collection of original file names (as they + were called on the remote user's file system). Note: only available if + files were extracted from the request body. Example: + + SecRule FILES "\.conf$" log,deny,status:403,phase:2 +
+ +
+ <literal moreinfo="none">FILES_COMBINED_SIZE</literal> + + Single value. Total size of the uploaded files. Note: only + available if files were extracted from the request body. Example: + + SecRule FILES_COMBINED_SIZE "@gt 1000" log,deny,status:403,phase:2 +
+ +
+ <literal moreinfo="none">FILES_NAMES</literal> + + Collection w/o parameter. Contains a list of form fields that were + used for file upload. Note: only available if files were extracted from + the request body. Example: + + SecRule FILES_NAMES "^upfile$" log,deny,status:403,phase:2 +
+ +
+ <literal moreinfo="none">FILES_SIZES</literal> + + Collection. Contains a list of file sizes. Useful for implementing + a size limitation on individual uploaded files. Note: only available if + files were extracted from the request body. Example: + + SecRule FILES_SIZES "@gt 100" log,deny,status:403,phase:2 +
+ +
+ <literal moreinfo="none">FILES_TMPNAMES</literal> + + Collection. Contains a collection of temporary files' names on the + disk. Useful when used together with @inspectFile. Note: only available if files + were extracted from the request body. Example: + + SecRule FILES_TMPNAMES "@inspectFile /usr/local/apache/tests/inspect_script.pl" +
+ +
+ <literal moreinfo="none">HTTP_</literal> + + This variable is a special prefix that is followed by a header + name and can be used to access any request header. Example: + + SecRule HTTP_REFERER "www\.badsite\.com" + + Note + + This variable is for backward-compatibilty with ModSecurity 1.X + rules. It has been superceded by the REQUEST_HEADERS variable + (REQUEST_HEADERS:Headername) +
+ +
+ <literal moreinfo="none">PATH_INFO</literal> + + Besides passing query information to a script/handler, you can + also pass additional data, known as extra path information, as part of + the URL. Example: + + SecRule PATH_INFO "^/(bin|etc|sbin|opt|usr)" +
+ +
+ <literal moreinfo="none">QUERY_STRING</literal> + + This variable holds form data passed to the script/handler by + appending data after a question mark. Example: + + SecRuleQ UERY_STRIN G"attack" +
+ +
+ <literal moreinfo="none">REMOTE_ADDR</literal> + + This variable holds the IP address of the remote client. + Example: + + SecRule REMOTE_ADDR "^192\.168\.1\.101$" +
+ +
+ <literal moreinfo="none">REMOTE_HOST</literal> + + If HostnameLookUps are set to On, then this variable will hold the + DNS resolved remote host name. If it is set to Off, then it will hold + the remote IP address. Possible uses for this variable would be to deny + known bad client hosts or network blocks, or conversely, to allow in + authorized hosts. Example: + + SecRule REMOTE_HOST "\.evil\.network\org$" +
+ +
+ <literal moreinfo="none">REMOTE_PORT</literal> + + This variable hold information on the source port that the client + used when initiating the connection to our web server. Example: in this + example, we are evaluating to see if the REMOTE_PORT + is less than 1024, which would indicate that the user is a privileged + user (root). + + SecRule REMOTE_PORT "@lt 1024" phase:1,log,pass,setenv:remote_port=privileged +
+ +
+ <literal moreinfo="none">REMOTE_USER</literal> + + This variable holds the username of the authenticated user. If + there are no password (basic|digest) access controls in place, then this + variable will be empty. Example: + + SecRule REMOTE_USER "admin" + + Note + + This data will not be available in a proxy-mode deployment as the + authentication is not local. +
+ +
+ <literal moreinfo="none">REQBODY_PROCESSOR</literal> + + Built-in processors are URLENCODED, + MULTIPART, and XML. + Example: + + SecRule REQBODY_PROCESSOR "^XML$ chain +SecRule XML "@validateDTD /opt/apache-frontend/conf/xml.dtd" +
+ +
+ <literal + moreinfo="none">REQBODY_PROCESSOR_ERROR</literal> + + 0 (no error) or 1 (error). If you want to stop processing on an + error you must have an explicit rule in phase 2 to do so. + Example: + + SecRule REQBODY_PROCESSOR_ERROR "@eq 1" deny,phase:2 +
+ +
+ <literal + moreinfo="none">REQBODY_PROCESSOR_ERROR_MSG</literal> + + Empty, or contains the error message from the processor. + Example: + + SecRule REQBODY_PROCESSOR_ERROR_MSG "failed to parse" t:lowercase +
+ +
+ <literal moreinfo="none">REQUEST_BASENAME</literal> + + This variable holds just the filename part of + REQUEST_FILENAME (e.g. index.php). Warning: not + urlDecoded. Example: + + SecRule REQUEST_BASENAME "^login\.php$" +
+ +
+ <literal moreinfo="none">REQUEST_BODY</literal> + + This variable holds the data in the request body (including + POST_PAYLOAD data). REQUEST_BODY should be used if the original order of + the arguements is important (ARGS should be used in all other cases). + Example: + + SecRule REQUEST_BODY "^username=\w{25,}\&password=\w{25,}\&Submit\=login$" + + Note + + This variable is only available if the content type is + application/x-www-form-urlencoded. +
+ +
+ <literal moreinfo="none">REQUEST_COOKIES</literal> + + This variable is a collection of all of the cookie data. Example: + the following example is using the Ampersand special operator to count + how many variables are in the collection. In this rule, it would trigger + if the request does not include any Cookie headers. + + SecRule &REQUEST_COOKIES "@eq 0" +
+ +
+ <literal moreinfo="none">REQUEST_COOKIES_NAMES</literal> + + This variable is a collection of the cookie names in the request + headers. Example: the following rule will trigger if the JSESSIONID + cookie is not present. + + SecRule &REQUEST_COOKIES_NAMES:JSESSIONID "@eq 0" +
+ +
+ <literal moreinfo="none">REQUEST_FILENAME</literal> + + This variable holds the relative REQUEST_URI minus the + QUERY_STRING part (e.g. /index.php). Warning: not urlDecoded. + Example: + + SecRule REQUEST_FILENAME "^/cgi-bin/login\.php$" +
+ +
+ <literal moreinfo="none">REQUEST_HEADERS</literal> + + This variable can be used as either a collection of all of the + Request Headers or can be used to specify indivudual headers (by using + REQUEST_HEADERS:Header-Name). Example: the first + example uses REQUEST_HEADERS as a collection and is applying the + validateUrlEncoding operator against all headers. + + SecRule REQUEST_HEADERS "@validateUrlEncoding" + + Example: the second example is targeting only the Host + header. + + SecRule REQUEST_HEADERS:Host "^[\d\.]+$" "deny,log,status:400,msg:'Host header is a numeric IP address'" +
+ +
+ <literal moreinfo="none">REQUEST_HEADERS_NAMES</literal> + + This variable is a collection of the names of all of the Request + Headers. Example: + + SecRule REQUEST_HEADERS_NAMES "^x-forwarded-for" "log,deny,status:403,t:lowercase,msg:'Proxy Server Used'" +
+ +
+ <literal moreinfo="none">REQUEST_LINE</literal> + + This variable holds the complete request line sent to the server + (including the REQUEST_METHOD and HTTP version data). Example: this + example rule will trigger if the request method is something other than + GET, HEAD, POST or if the HTTP is something other than HTTP/0.9, 1.0 or + 1.1. + + SecRule REQUEST_LINE "!(^((?:(?:pos|ge)t|head))|http/(0\.9|1\.0|1\.1)$)" + + Note + + Due to the default action transformation function lowercase, the + regex strings should be in lowercase as well unless the t:none + transformation function is specified for this particular rule. +
+ +
+ <literal moreinfo="none">REQUEST_METHOD</literal> + + This variable holds the Request Method used by the client. + Example: the following example will trigger if the Request Method is + either CONNECT or TRACE. + + SecRule REQUEST_METHOD "^((?:connect|trace))$" + + Note + + Due to the default action transformation function lowercase, the + regex strings should be in lowercase as well unless the t:none + transformation function is specified for this particular rule. +
+ +
+ <literal moreinfo="none">REQUEST_PROTOCOL</literal> + + This variable holds the Request Protocol Version information. + Example: + + SecRule REQUEST_PROTOCOL "!^http/(0\.9|1\.0|1\.1)$" + + Note + + Due to the default action transformation function lowercase, the + regex strings should be in lowercase as well unless the t:none + transformation function is specified for this particular rule. +
+ +
+ <literal moreinfo="none">REQUEST_URI</literal> + + This variable holds the full URL including the QUERY_STRING data + (e.g. /index.php?p=X), however it will never contain a domain name, even + if it was provided on the request line. Warning: not urlDecoded. It also + does not include either the REQUEST_METHOD or the HTTP version info. + Example: + + SecRule REQUEST_URI "attack" +
+ +
+ <literal moreinfo="none">REQUEST_URI_RAW</literal> + + Same as REQUEST_URI but will contain the domain name if it was + provided on the request line (e.g. + http://www.example.com/index.php?p=X). Warning: not urlDecoded. + Example: + + SecRule REQUEST_URI_RAW "http:/" +
+ +
+ <literal moreinfo="none">RESPONSE_BODY</literal> + + This variable holds the data for the response payload. + Example: + + SecRule RESPONSE_BODY "ODBC Error Code" +
+ +
+ <literal moreinfo="none">RESPONSE_HEADERS</literal> + + This variable is similar to the REQUEST_HEADERS variable and can + be used in the same manner. Example: + + SecRule RESPONSE_HEADERS:X-Cache "MISS" + + Note + + This variable may not have access to some headers when running in + embedded-mode. Headers such as Server, Date and Connection are added + during a later Apache hook just prior to sending the data to the client. + This data should be available, however, when running in + proxy-mode. +
+ +
+ <literal moreinfo="none">RESPONSE_HEADERS_NAMES</literal> + + This variable is a collection of the response header names. + Example: + + SecRule RESPONSE_HEADERS_NAMES "Set-Cookie" + + Note + + Same limitations as RESPONSE_HEADERS with regards to access to + some headers in embedded-mode. +
+ +
+ <literal moreinfo="none">RESPONSE_PROTOCOL</literal> + + This variable holds the HTTP Response Protocol information. + Example: + + SecRule RESPONSE_PROTOCOL "^HTTP\/0\.9" +
+ +
+ <literal moreinfo="none">RESPONSE_STATUS</literal> + + This variable holds the HTTP Response Status Code generated by + Apache. Example: + + SecRule RESPONSE_STATUS "^[45]" + + Note + + This directive may not work as expected in embedded-mode as Apache + handles many of the stock response codes (404, 401, etc...) earlier in + Phase 2. This variable should work as expected in a proxy-mode + deployment. +
+ +
+ <literal moreinfo="none">RULE</literal> + + This variable provides access to theid,rev,severity, andmsgfields of the rule that triggered the + action. Only available for expansion in action strings (e.g.setvar:tx.varname=%{rule.id}).Example: + + SecRule &REQUEST_HEADERS:Host "@eq 0" "phase:2,deny,id:1,setvar:tx.varname=%{rule.id}" +
+ +
+ <literal moreinfo="none">SCRIPT_BASENAME</literal> + + This variable holds just the local filename part of + SCRIPT_FILENAME. Example: + + SecRule SCRIPT_BASENAME "^login\.php$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_FILENAME</literal> + + This variable holds the full path on the server to the requested + script. (e.g. SCRIPT_NAME plus the server path). Example: + + SecRule SCRIPT_FILENAME "^/usr/local/apache/cgi-bin/login\.php$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_GID</literal> + + This variable holds the groupid (numerical value) of the group + owner of the script. Example: + + SecRule SCRIPT_GID "!^46$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_GROUPNAME</literal> + + This variable holds the group name of the group owner of the + script. Example: + + SecRule SCRIPT_GROUPNAME "!^apache$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_MODE</literal> + + This variable holds the script's permissions mode data (numerical + - 1=execute, 2=write, 4=read and 7=read/write/execute). Example: will + trigger if the script has the WRITE permissions set. + + SecRule SCRIPT_MODE "^(2|3|6|7)$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_UID</literal> + + This variable holds the userid (numerical value) of the owner of + the script. Example: the example rule below will trigger if the UID is + not 46 (the Apache user). + + SecRule SCRIPT_UID "!^46$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SCRIPT_USERNAME</literal> + + This variable holds the username of the owner of the script. + Example: + + SecRule SCRIPT_USERNAME "!^apache$" + + Note + + This variable is not available in proxy mode. +
+ +
+ <literal moreinfo="none">SERVER_ADDR</literal> + + This variable contains the IP address of the server. + Example: + + SecRule SERVER_ADDR "^192\.168\.1\.100$" +
+ +
+ <literal moreinfo="none">SERVER_NAME</literal> + + This variable contains the server's hostname or IP address. + Example: + + SecRule SERVER_NAME "hostname\.com$" + + Note + + This data is taken from the Host header submitted in the client + request. +
+ +
+ <literal moreinfo="none">SERVER_PORT</literal> + + This variable contains the local port that the web server is + listening on. Example: + + SecRuleS ERVER_PORT "^80$" +
+ +
+ <literal moreinfo="none">SESSION</literal> + + This variable is a collection, available only after setsid is executed. Example: the following + example shows how to initialize a SESSION collection with setsid, how to + use setvar to increase the session.score values, how to set the + session.blocked variable and finally how to deny the connection based on + the session:blocked value. + + SecRule REQUEST_COOKIES:PHPSESSID !^$ chain,nolog,pass +SecAction setsid:%{REQUEST_COOKIES.PHPSESSID} +SecRule REQUEST_URI "^/cgi-bin/finger$" "pass,log,setvar:session.score=+10" +SecRule SESSION:SCORE "@gt 50" "pass,log,setvar:session.blocked=1" +SecRule SESSION:BLOCKED "@eq 1" "log,deny,status:403" +
+ +
+ <literal moreinfo="none">SESSIONID</literal> + + This variable is the value set with setsid. Example: + + SecRule SESSIONID !^$ chain,nolog,pass +SecRule REQUEST_COOKIES:PHPSESSID !^$ +SecAction setsid:%{REQUEST_COOKIES.PHPSESSID} +
+ +
+ <literal moreinfo="none">TIME</literal> + + This variable holds a formatted string representing the time + (hour:minute:second). Example: + + SecRule TIME "^(([1](8|9))|([2](0|1|2|3))):\d{2}:\d{2}$" +
+ +
+ <literal moreinfo="none">TIME_DAY</literal> + + This variable holds the current date (1-31). Example: this rule + would trigger anytime between the 10th and 20th days of the + month. + + SecRule TIME_DAY "^(([1](0|1|2|3|4|5|6|7|8|9))|20)$" +
+ +
+ <literal moreinfo="none">TIME_EPOCH</literal> + + This variable holds the time in seconds since 1970. + Example: + + SecRule TIME_EPOCH "@gt 1000" +
+ +
+ <literal moreinfo="none">TIME_HOUR</literal> + + This variable holds the current hour (0-23). Example: this rule + would trigger during "off hours". + + SecRule TIME_HOUR "^(0|1|2|3|4|5|6|[1](8|9)|[2](0|1|2|3))$" +
+ +
+ <literal moreinfo="none">TIME_MIN</literal> + + This variable holds the current minute (0-59). Example: this rule + would trigger during the last half hour of every hour. + + SecRule TIME_MIN "^(3|4|5)" +
+ +
+ <literal moreinfo="none">TIME_MON</literal> + + This variable holds the current month (0-11). Example: this rule + would match if the month was either November (10) or December + (11). + + SecRule TIME_MON "^1" +
+ +
+ <literal moreinfo="none">TIME_SEC</literal> + + This variable holds the current second count (0-59). + Example: + + SecRule TIME_SEC "@gt 30" +
+ +
+ <literal moreinfo="none">TIME_WDAY</literal> + + This variable holds the current weekday (0-6). Example: this rule + would trigger only on week-ends (Saturday and Sunday). + + SecRule TIME_WDAY "^(0|6)$" +
+ +
+ <literal moreinfo="none">TIME_YEAR</literal> + + This variable holds the current four-digit year data. + Example: + + SecRule TIME_YEAR "^2006$" +
+ +
+ <literal moreinfo="none">TX</literal> + + Transaction Collection. This is used to store pieces of data, + create a transaction anomaly score, and so on. Transaction variables are + set for 1 request/response cycle. The scoring and evaluation will not + last past the current request/response process. Example: In this + example, we are using setvar to increase the tx.score value by 5 points. + We then have a follow-up run that will evaluate the transactional score + this this request and then it will decided whether or not to allow/deny + the request through. + + SecRule WEBSERVER_ERROR_LOG "File does not exist" "phase:5,pass,setvar:tx.score=+5" +SecRule TX:SCORE "@gt 20" deny,log +
+ +
+ <literal moreinfo="none">USERID</literal> + + This variable is the value set with setuid. Example: + + SecAction setuid:%{REMOTE_USER},nolog +SecRule USERID "Admin" +
+ +
+ <literal moreinfo="none">WEBAPPID</literal> + + This variable is the value set with SecWebAppId. Example: + + SecWebAppId "WebApp1" +SecRule WEBAPPID "WebApp1" "chain,log,deny,status:403" +SecRule REQUEST_HEADERS:Transfer-Encoding "!^$" +
+ +
+ <literal moreinfo="none">WEBSERVER_ERROR_LOG</literal> + + Contains zero or more error messages produced by the web server. + Access to this variable is in phase:5 (logging). Example: + + SecRule WEBSERVER_ERROR_LOG "File does not exist" "phase:5,setvar:tx.score=+5" +
+ +
+ <literal moreinfo="none">XML</literal> + + Can be used standalone (as a target for validateDTD and + validateSchema) or with an XPath expression parameter (which makes it a + valid target for any function that accepts plain text). Example using + XPath: + + SecDefaultAction log,deny,status:403,phase:2 +SecRule REQUEST_HEADERS:Content-Type ^text/xml$ phase:1,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML +SecRule REQBODY_PROCESSOR "!^XML$" skip:2 +SecRule XML:/employees/employee/name/text() Fred +SecRule XML:/xq:employees/employee/name/text() Fred xmlns:xq=http://www.example.com/employees +
+
+ +
+ Transformation functions + + When ModSecurity receives request or response information, it makes + a copy of this data and places it into memory. It is on this data in + memory that transformation functions are applied. The raw request/response + data is never altered. Transformation functions are used to transform a + variable before testing it in a rule. + + Note + + The default transformation function setting is - lowercase, + replaceNulls and compressWhitespace (in this order). + + The following rule will ensure that an attacker does not use mixed + case in order to evade the ModSecurity rule: + + SecRule ARG:p "xp_cmdshell" "t:lowercase"multipetranformation + actions can be used in the same rule, for example the following rule also + ensures that an attacker does not use URL encodign (%xx encoding) for + evasion. Not the order of the transformation functions, which ensures that + a URL encoded letter is first decoded and than translated to lower + case. + + SecRule ARG:p "xp_cmdshell" "t:urlDecode,t:lowercase" + + One can use the SetDefaultAction command to ensure the translation + occurs for every rule until the next. Note that translation actions are + additive, so if a rule explicitly list actions, the translation actions + set by SetDefaultAction are still performed. + + SecDefaultAction t:urlDecode,t:lowercase + + The following transformation functions are supported: + +
+ <literal>base64Decode</literal> + + This function decoes a base64-encoded string. +
+ +
+ <literal>base64Encode</literal> + + This function encodes input string using base64 encoding. +
+ +
+ <literal>compressWhitespace</literal> + + This function is enabled by default. It converts whitespace + characters (32, \f, \t, \n, \r, \v, 160) to spaces (ASCII 32) and then + compresses multiple space characters into only one. +
+ +
+ <literal>escapeSeqDecode</literal> + + This function decode ANSI C escape sequences:\a,\b,\f,\n,\r,\t,\v,\\,\?,\',\",\xHH(hexadecimal),\0OOO(octal). Invalid encodings are left in + the output. +
+ +
+ <literal>hexDecode</literal> + + This function decodes a hex-encoded string. +
+ +
+ <literal>hexEncode</literal> + + This function encodes input as hex-encoded string. +
+ +
+ <literal>htmlEntityDecode</literal> + + This function decodes HTML entities present in input. The + following variants are supported: + + + + &#xHHand&#xHH;(where H is any hexadecimal + number) + + + + &#DDDand&#DDD;(where D is any decimal + number) + + + + &quotand&quot; + + + + &nbsp and&nbsp; + + + + &ltand&lt; + + + + &gtand&gt; + + +
+ +
+ <literal>lowercase</literal> + + This function is enabled by default. It converts all charactes to + lowercase using the current C locale. +
+ +
+ <literal>md5</literal> + + This function calculates an MD5 hash from input. +
+ +
+ <literal><literal>none</literal></literal> + + This not an actual transformation function but an instruction to + ModSecurity to remove all transformation functions associated with the + current rule and start from scratch. +
+ +
+ <literal>normalisePath</literal> + + This function will remove multiple slashes, self-references and + directory back-references (except when they are at the beginning of the + path). +
+ +
+ <literal>normalisePathWin</literal> + + Same as normalisePath, but will first convert backslash characters + to forward slashes. +
+ +
+ <literal>removeNulls</literal> + + This function removes NULL bytes from input. +
+ +
+ <literal>removeWhitespace</literal> + + This function removes all whitespace characters. +
+ +
+ <literal>replaceComments</literal> + + This function replaces each occurence of a C-style comments + (/* ... */) with a single space + (multiple consecutive occurences of a space will not be compressed). + Unterminated comments will too be replaced with a space (ASCII 32). + However, a standalone termination of a comment (*/) will not be acted upon. +
+ +
+ <literal>replaceNulls</literal> + + This function is enabled by default. It replaces NULL bytes in + input with spaces (ASCII 32). +
+ +
+ <literal>urlDecode</literal> + + This function decodes an URL-encoded input string. Invalid + encodings (i.e. the ones that use non-hexadecimal characters, or the + ones that are at the end of string and have one or two characters + missing) will not be converted. If you want to detect invalid encodings + use the @validateUrlEncoding + operator. The transformational function should not be used against + variables that have already been URL-decoded unless it is your intention + to perform URL decoding twice! +
+ +
+ <literal>urlDecodeUni</literal> + + In addition to decoding %xx like urlDecode, urlDecodeUni also decodes %uXXXX encoding (only the + lower byte will be used, the higher byte will be discarded). +
+ +
+ <literal>urlEncode</literal> + + This function encodes input using URL encoding. +
+ +
+ <literal>sha1</literal> + + This function calculates a SHA1 hash from input. +
+
+ +
+ Actions + + Each action belongs to one of five groups: + + + + Disruptive actions- are those actions where + ModSecurity will intercept the data. They can only appear in the first + rule in a chain. + + + + Non-disruptive actions; can appear + anywhere. + + + + Flow actions; can appear only in the first + rule in a chain. + + + + Meta-data actions(id, + rev, severity, msg); can only appear in the first rule in + a chain. + + + + Data actions- can appear anywhere; these + actions are completely passive and only serve to carry data used by + other actions. + + + +
+ <literal>allow</literal> + + Description: Stops processing on + a successful match and allows transaction to proceed. + + Action Group: Disruptive + + Example: + + SecRule REMOTE_ADDR "^192\.168\.1\.100$" nolog,phase:1,allow + + Note + + The allow action only applies to the current processing phase. If + your intent is to explicitly allow a request, then you should use the + "ctl" action to turn the ruleEngine off - + ctl:ruleEngine=Off. +
+ +
+ <literal>auditlog</literal> + + Description: Marks the + transaction for logging in the audit log. + + Action Group: + Non-Disruptive + + Example: + + SecRule REMOTE_ADDR "^192\.168\.1\.100$" auditlog,phase:1,allow + + Note + + The auditlog action is now explicit if log is already + specified. +
+ +
+ <literal>capture</literal> + + Description: When used together + with the regular expression operator, capture action will create copies + of regular expression captures and place them into the transaction + variable collection. Up to ten captures will be copied on a successful + pattern match, each with a name consisting of a digit from 0 to + 9. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_BODY "^username=(\w{25,})" phase:2,capture,t:none,chain +SecRule TX:1 "(?:(?:a(dmin|nonymous)))" + + Note + + The 0 data captures the entire REGEX match and 1 captures the data + in the first parantheses, etc... +
+ +
+ <literal>chain</literal> + + Description: Chains the rule + where the action is placed with the rule that immediately follows it. + The result is called a rule chain. Chained rules + allow for more complex rule matches where you want to use a number of + different VARIABLES to create a better rule and to help prevent false + positives. + + Action Group: Flow + + Example: + + # Refuse to accept POST requests that do +# not specify request body length +SecRule REQUEST_METHOD ^POST$ chain +SecRule REQUEST_HEADER:Content-Length ^$ + + Note + + In programming language concepts, think of chained rules somewhat + similar to AND conditional statements. The actions specified in the + first portion of the chained rule will only be triggered if all of the + variable checks return positive hits. If one aspect of the chained rule + is negative, then the entire rule chain is negative. Also note that + disruptive actions, execution phases, metadata actions (id, rev, msg) + and skip actions can only be specified on by the chain starter + rule. +
+ +
+ <literal>ctl</literal> + + Description: The ctl action + allows configuration options to be updated for the transaction. + + Action Group: + Non-Disruptive + + Example: + + # Parse requests with Content-Type "text/xml" as XML +SecRule REQUEST_CONTENT_TYPE ^text/xml nolog,pass,ctl:requestBodyProcessor=XML + + Note + + The following configuration options are supported: + + + + auditEngine + + + + auditLogParts + + + + debugLogLevel + + + + requestBodyAccess + + + + requestBodyLimit + + + + requestBodyProcessor + + + + responseBodyAccess + + + + responseBodyLimit + + + + ruleEngine + + + + With the exception of + requestBodyProcessor, each configuration option corresponds to + one configuration directive and the usage is identical. + + The requestBodyProcessor option allows you to configure the + request body processor. By default ModSecurity will use the URLENCODED and + MULTIPART processors to process an application/x-www-form-urlencoded and a + multipart/form-data body, + respectively. A third processor, XML, is also supported, but it is never + used implicitly. Instead you must tell ModSecurity to use it by placing + a few rules in the REQUEST_HEADERS + processing phase. After the request body was processed as XML you will + be able to use the XML-related features to inspect it. + + Request body processors will not interrupt a transaction if an + error occurs during parsing. Instead they will set variables REQBODY_PROCESSOR_ERROR and REQBODY_PROCESSOR_ERROR_MSG. These variables + should be inspected in the REQUEST_BODY phase and an appropriate action + taken. +
+ +
+ <literal>deny</literal> + + Description: Stops rule + processing and intercepts transaction. + + Action Group: Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "nikto" log,deny,msg:'Nikto Scanners Identified" +
+ +
+ <literal>deprecatevar</literal> + + Description: Decrement counter + based on its age. + + Action Group: + Non-Disruptive + + Example: The following example will decrement the counter by 60 + every 300 seconds. + + SecAction deprecatevar:session.score=60/300 + + Note + + Counter values are always positive, meaning the value will never + go below zero. +
+ +
+ <literal>drop</literal> + + Description: Immediately initiate + a "connection close" action to tear down the TCP connection by sending a + FIN packet. + + Action Group: Disruptive + + Example: The following example initiates an IP collection for + tracking Basic Authentication attempts. If the client goes over the + threshold of more than 25 attempts in 2 minutes, it will DROP subsequent + connections. + + SecAction initcol:ip=%{REMOTE_ADDR},nolog +SecRule ARGS:login "!^$" nolog,phase:1,setvar:ip.auth_attempt=+1,deprecatevar:ip.auth_attempt=20/120 +SecRule IP:AUTH_ATTEMPT "@gt 25" log,drop,phase:1,msg:'Possible Brute Force Attack" + + Note + + This action is extremely useful when responding to both Brute + Force and Denial of Service attacks in that, in both cases, you want to + minimize both the network bandwidth and the data returned to the client. + This action causes error message to appear in the log "(9)Bad file + descriptor: core_output_filter: writing data to the network" +
+ +
+ <literal>exec</literal> + + Description: Executes an external + script/binary supplied as parameter. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_URI "^/cgi-bin/script\.pl" "log,exec:/usr/local/apache/bin/test.sh,phase:1" + + Note + + This directive does not effect a primary action if it exists. This + action will always call script with no parameters, but providing all + information in the environment. All the usual CGI environment variables + will be there. You can have one binary executed per filter match. + Execution will add the header mod_security-executed to the list of + request headers. You should be aware that forking a threaded process + results in all threads being replicated in the new process. Forking can + therefore incur larger overhead in multithreaded operation. The script + you execute must write something (anything) to stdout. If it doesn't + ModSecurity will assume execution didn't work. +
+ +
+ <literal>expirevar</literal> + + Description: Configurescollection + variable to expire after the given time in seconds. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_COOKIES:JSESSIONID "!^$" nolog,phase:1,pass,chain +SecAction setsid:%{REQUEST_COOKIES:JSESSIONID} +SecRule REQUEST_URI "^/cgi-bin/script\.pl" "log,allow,setvar:session.suspicious=1,expirevar:session.suspicious=3600,phase:1" + + Note + + You should use expirevar actions at the same time that you use + setvar actions in order to keep the indended expiration time. If they + are used on their own (perhaps in a SecAction directive) the expire time + could get re-set. When variables are removed from collections, and there + are no other changes, collections are not written to disk at the end of + request. This is because the variables can always be expired again when + the collection is read again on a subsequent request. +
+ +
+ <literal>id</literal> + + Description: Assigns a unique ID + to the rule or chain. + + Action Group: Metadata + + Example: + + SecRule &REQUEST_HEADERS:Host "@eq 0" "log,id:60008,severity:2,msg:'Request Missing a Host Header'" + + Note + + These are the reserved ranges: + + + + 1 – 99999; reserved for your internal needs, use as you see + fit but don't publish them to others + + + + 100,000-199,999; reserved for internal use of the engine, to + assign to rules that do not have explicit IDs + + + + 200,000-299,999; reserved for rules published at + modsecurity.org + + + + 300,000-399,999; reserved for rules published at + gotroot.com + + + + 400,000 and above; unreserved range. + + +
+ +
+ <literal>initcol</literal> + + Description: Initialises a named + persistent collection, either by loading data from storage or by + creating a new collection in memory. + + Action Group: + Non-Disruptive + + Example: The following example initiates IP address + tracking. + + SecAction initcol:ip=%{REMOTE_ADDR},nolog + + Note + + Every collection contains several built-in variables that are + read-only: + + + + CREATE_TIME- date/time of + the creation of the collection. + + + + KEY- the value of the + initcol variable (the client's IP address in the example). + + + + LAST_UPDATE_TIME- date/time + of the last update to the collection. + + + + TIMEOUT- date/time in + seconds when the collection will be updated on disk from memory (if + no other updates occur). + + + + UPDATE_COUNTER- how many + times the collection has been updated since creation. + + + + UPDATE_RATE- is the average + rate updates per minute since creation. + + + + Collections are loaded into memory when the initcol action is + encountered. The collection in storage will be updated (and the + appropriate counters increased)onlyif it was + changed during transaction processing. + + + To create a collection to hold session variables (SESSION) use action setsid. To create a collection to hold user + variables (USER)use action setuid. + + + + At this time it is only possible to have three + collections: IP, SESSION, and USER. + +
+ +
+ <literal>log</literal> + + Description: Indicates that a + successful match of the rule needs to be logged. + + Action Group: + Non-Disruptive + + Example: + + SecAction initcol:ip=%{REMOTE_ADDR},log + + Note + + This action will log matches to the Apache error log file and the + ModSecurity audit log. +
+ +
+ <literal>msg</literal> + + Description: Assigns a custom + message to the rule or chain. + + Action Group: Metadata + + Example: + + SecRule &REQUEST_HEADERS:Host "@eq 0" "log,id:60008,severity:2,msg:'Request Missing a Host Header'" + + Note + + The msg information appears in the error and/or audit log files + and is not sent back to the client in response headers. +
+ +
+ <literal>multiMatch</literal> + + Description: If enabled + ModSecurity will perform multiple operator invocations for every target, + before and after every anti-evasion transformation is performed. + + Action Group: + Non-Disruptive + + Example: + + SecDefaultAction log,deny,phase:1,t:lowercase,t:removeNulls,t:lowercase SecRule ARGS "attack"multiMatch + + Note + + Normally, variables are evaluated once, only after all + transformation functions have completed. With multiMatch, variables are + checked against the operator before and after every transformation + function that changes the input. +
+ +
+ <literal>noauditlog</literal> + + Description: Indicates that a + successful match of the rule should not be used as criteria whether the + transaction should be logged to the audit log. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" allow,noauditlog + + Note + + If the SecAuditEngine is set to On, all of the transactions will + be logged. If it is set to RelevantOnly, then you can control it with + the noauditlog action. Even it the noauditlog action is applied to a + specific rule, if a rule either before or after triggered an audit + event, then the tranaction will be logged to the audit log. The correct + way to disable audit logging for the entire transaction is to use + "ctl:auditEngine=Off" +
+ +
+ <literal>nolog</literal> + + Description: Prevents rule + matches from appearing in both the error and audit logs. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" allow,nolog + + Note + + The nolog action also implies noauditlog. +
+ +
+ <literal>pass</literal> + + Description: Continues processing + with the next rule in spite of a successful match. + + Action Group: Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" log,pass + + Note + + Transaction will not be interrupted but it will be logged (unless + logging has been suppressed). +
+ +
+ <literal>pause</literal> + + Description: Pauses transaction + processing for the specified number of milliseconds. + + Action Group: Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" log,deny,status:403,pause:5000 + + Note + + This feature can be of limited benefit for slowing down Brute + Force Scanners, however use with care. If you are under a Denial of + Service type of attack, the pause feature may make matters worse as this + feature will cause child processes to sit idle until the pause is + completed. +
+ +
+ <literal>phase</literal> + + Description: Places the rule (or + the rule chain) into one of five available processing phases. + + Action Group: Disruptive + + Example: + + SecDefaultAction log,deny,phase:1,t:lowercase,t:removeNulls,t:lowercase +SecRule REQUEST_HEADERS:User-Agent "Test" log,deny,status:403 + + Note + + Keep in mind that is you specify the incorrect phase, the target + variable that you specify may be empty. This could lead to a false + negative situation where your variable and operator (RegEx) may be + correct, but it misses malicious data because you specified the wrong + phase. +
+ +
+ <literal>proxy</literal> + + Description: Intercepts + transaction by forwarding request to another web server using the proxy + backend. + + Action Group: Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" log,proxy:http://www.honeypothost.com/ + + Note + + For this action to work, mod_proxy must also be installed. This + action is useful if you would like to proxy matching requests onto a + honeypot webserver. +
+ +
+ <literal>redirect</literal> + + Description: Intercepts + transaction by issuing a redirect to the given location. + + Action Group: Disruptive + + Example: + + SecRule REQUEST_HEADERS:User-Agent "Test" log,redirect:http://www.hostname.com/failed.html + + Note + + If thestatusaction is present + and its value is acceptable (301, 302, 303, or 307) it will be used for + the redirection. Otherwise status code 302 will be used. +
+ +
+ <literal>rev</literal> + + Description: Specifies rule + revision. + + Action Group: Metadata + + Example: + + SecRule REQUEST_METHOD "^PUT$" "id:340002,rev:1,severity:2,msg:'Restricted HTTP function'" + + Note + + This action is used in combination with theidaction to allow the same rule ID to be used + after changes take place but to still provide some indication the rule + changed. +
+ +
+ <literal>sanitiseArg</literal> + + Description: Sanitises (replaces + each byte with an asterisk) a named request argument prior to audit + logging. + + Action Group: + Non-Disruptive + + Example: + + SecAction nolog,phase:2,sanitiseArg:password + + Note + + The sanitize actions do not sanitize any data within the actual + raw requests but only on the copy of data within memory that is set to + log to the audit log. It will not sanitize the data in the + modsec_debug.log file (if the log level is set high enough to capture + this data). +
+ +
+ <literal>sanitiseMatched</literal> + + Description: Sanitises the + variable (request argument, request header, or response header) that + caused a rule match. + + Action Group: + Non-Disruptive + + Example: This action can be used to sanitise arbitrary transaction + elements when they match a condition. For example, the example below + will sanitise any argument that contains the word + password in the name. + + SecRule ARGS_NAMES password nolog,pass,sanitiseMatched + + Note + + Same note as sanitiseArg. +
+ +
+ <literal>sanitiseRequestHeader</literal> + + Description: Sanitises a named + request header. + + Action Group: + Non-Disruptive + + Example: For example, the example below will sanitise the data in + the Authorization header. + + SecAction log,phase:1,sanitiseRequestHeader:Authorization + + Note + + Same note as sanitiseArg. +
+ +
+ <literal>sanitiseResponseHeader</literal> + + Description: Sanitises a named + response header. + + Action Group: + Non-Disruptive + + Example: For example, the example below will sanitise the + Set-Cookie data sent to the client. + + SecAction log,phase:3,sanitiseResponseHeader:Set-Cookie + + Note + + Same note as sanitiseArg. +
+ +
+ <literal>severity</literal> + + Description: Assigns severity to + the rule it is placed with. + + Action Group: Metadata + + Example: + + SecRule REQUEST_METHOD "^PUT$" "id:340002,rev:1,severity:2,msg:'Restricted HTTP function'" + + Note + + The severity numbers follow the Syslog convention - + + + + 0 = EMERGENCY + + + + 1 = ALERT + + + + 2 = CRITICAL + + + + 3 = ERROR + + + + 4 = WARNING + + + + 5 = NOTICE + + + + 6 = INFO + + + + 7 = DEBUG + + +
+ +
+ <literal>setuid</literal> + + Description: + Special-purposeaction that initialises the USER collection. + + Action Group: + Non-Disruptive + + Example: + + SecAction setuid:%{REMOTE_USER},nolog + + Note + + After initialisation takes place the variable USERID will be available for use in the + subsequent rules. +
+ +
+ <literal>setsid</literal> + + Description: + Special-purposeaction that initialises the SESSION collection. + + Action Group: + Non-Disruptive + + Example: + + # Initialise session variables using the session cookie value +SecRule REQUEST_COOKIES:PHPSESSID !^$ chain,nolog,pass +SecActionsetsid:%{REQUEST_COOKIES.PHPSESSID} + + Note + + On first invocation of this action the collection will be empty + (not taking the pre-defined variables into account - seeinitcolfor more information). On subsequent + invocations the contents of the collection (session, in this case) will + be retrieved from storage. After initialisation takes place the + variable SESSIONID will be available + for use in the subsequent rules.This action understands each application + maintains its own set of sessions. It will utilise the current web + application ID to create a session namespace. +
+ +
+ <literal>setenv</literal> + + Description: Creates, removes, or + updates an environment variable. + + Action Group: + Non-Disruptive + + Examples: + + To create a new variable (if you omit the value 1 will be used): + + setenv:name=value + + To remove a variable: + + setenv:!name + + Note + + This action can be used to establish communication with other + Apache modules. +
+ +
+ <literal>setvar</literal> + + Description: Creates, removes, or + updates a variable in the specified collection. + + Action Group: + Non-Disruptive + + Examples: + + To create a new variable: + + setvar:tx.score=10 + + To remove a variable prefix the name with exclamation mark: + + setvar:!tx.score + + To increase or decrease variable value use+and-characters in front of a numerical + value: + + setvar:tx.score=+5 +
+ +
+ <literal>skip</literal> + + Description: Skips one or more + rules (or chains) on successful match. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_URI "^/$" "chain,skip:2" +SecRule REMOTE_ADDR "^127\.0\.0\.1$" "chain" +SecRule REQUEST_HEADERS:User-Agent "^Apache \(internal dummy connection\)$" "t:none" +SecRule &REQUEST_HEADERS:Host "@eq 0" "deny,log,status:400,id:960008,severity:4,msg:'Request Missing a Host Header'" +SecRule &REQUEST_HEADERS:Accept "@eq 0" "log,deny,log,status:400,id:960015,msg:'Request Missing an Accept Header'" + + Note + + Skip only applies to the current processing phase and not + necessarily the order in which the rules appear in the configuration + file. If you group rules by processing phases, then skip should work as + expected. This action can not be used to skip rules within one chain. + Accepts a single paramater denoting the number of rules (or chains) to + skip. +
+ +
+ <literal>status</literal> + + Description: Specifies the + response status code to use with actions + deny and redirect. + + Action Group: Disruptive + + Example: + + SecDefaultAction log,deny,status:403,phase:1 + + Note + + Staus actions defined in Apache scope locations (such as + Directory, Location, etc...) may be superceded by phase:1 action + settings. The Apache ErrorDocument directive will be triggered if + present in the configuration. Therefore if you have previously defined a + custom error page for a given status then it will be executed and its + output presented to the user. +
+ +
+ <literal>t</literal> + + Description: This action can be + used which transformation function should be used against the specified + variables before they (or the results, rather) are run against the + operator specified in the rule. + + Action Group: + Non-Disruptive + + Example: + + SecDefaultAction log,deny,phase:1,t:lowercase,t:removeNulls,t:lowercase +SecRule REQUEST_COOKIES:SESSIONID "47414e81cbbef3cf8366e84eeacba091" log,deny,status:403,t:md5 + + Note + + Any transformation functions that you specify in a SecRule will be + in addtion to previous ones specified in SecDefaultAction. Use of + "t:none" will remove all transformation functions for the specified + rule. +
+ +
+ <literal>xmlns</literal> + + Description: This action should + be used together with an XPath expression to register a + namespace. + + Action Group: + Non-Disruptive + + Example: + + SecRule REQUEST_HEADERS:Content-Type "text/xml" phase:1,pass,ctl:requestBodyProcessor=XML,ctl:requestBodyAccess=On,xmlns:xsd="http://www.w3.org/2001/XMLSchema" +SecRule XML:/soap:Envelope/soap:Body/q1:getInput/id() "123" phase:2,deny +
+
+ +
+ Operators + + A number of operators can be used in rules, as documented below. The + operator syntax used the "@" symbol followed by the specific operator + name. + +
+ <literal>eq</literal> + + Description: This operator is a + numerical comparison and stands for "equal to." + + Example: + + SecRule &REQUEST_HEADERS_NAMES "@eq 15" +
+ +
+ <literal>ge</literal> + + Description: This operator is a + numerical comparison and stands for "greater than or equal to." + + Example: + + SecRule &REQUEST_HEADERS_NAMES "@ge 15" +
+ +
+ <literal>gt</literal> + + Description: This operator is a + numerical comparison and stands for "greater than." + + Example: + + SecRule &REQUEST_HEADERS_NAMES "@gt 15" +
+ +
+ <literal>inspectFile</literal> + + Description: Executes the + external script/binary given as parameter to the operator against every + file extracted from the request. + + Example: + + SecRule FILES_TMPNAMES "@inspectFile /opt/apache/bin/inspect_script.pl" +
+ +
+ <literal>le</literal> + + Description: This operator is a + numerical comparison and stands for "less than or equal to." + + Example: + + SecRule &REQUEST_HEADERS_NAMES "@le 15" +
+ +
+ <literal>lt</literal> + + Description: This operator is a + numerical comparison and stands for "less than." + + Example: + + SecRule &REQUEST_HEADERS_NAMES "@lt 15" +
+ +
+ <literal>rbl</literal> + + Description: Look up the + parameter in the RBL given as parameter. Parameter can be an IPv4 + address, or a hostname. + + Example: + + SecRule REMOTE_ADDR "@rbl sc.surbl.org" +
+ +
+ <literal>rx</literal> + + Description: Regular expression + operator. This is the default operator, so if the "@" operator is not + defined, it is assumed to be rx. + + Example: + + SecRule REQUEST_HEADERS:User-Agent "@rx nikto" + + Note + + Regular expressions are handled by the PCRE library (http://www.pcre.org). ModSecurity + compiles its regular expressions with the following settings: + + + + The entire input is treated as a single line, even when there + are newline characters present. + + + + All matches are case-sensitive. If you do not care about case + sensitivity you either need to implement the lowercase transformational function, or + use the per-pattern(?i)modificator, as allowed by + PCRE. + + + + The PCRE_DOTALL flag is set + during compilation, meaning a single dot will match any character, + including the newlines. + + +
+ +
+ <literal>validateByteRange</literal> + + Description: Validates the byte + range used in the variable falls into the specified range. + + Example: + + SecRule ARG:text "@validateByteRange 10, 13, 32-126" + + Note + + You can force requests to consist only of bytes from a certain + byte range. This can be useful to avoid stack overflow attacks (since + they usually contain "random" binary content). Default range values are + 0 and 255, i.e. all byte values are allowed. This directive does not + check byte range in a POST payload when multipart/form-data encoding + (file upload) is used. Doing so would prevent binary files from being + uploaded. However, after the parameters are extracted from such request + they are checked for a valid range. + + validateByteRange is similar to the ModSecurity 1.X + SecFilterForceByteRange Directive however since it works in a rule + context, it has the following differences: + + + + You can specify a different range for different + variables. + + + + It has an "event" context (id, msg....) + + + + It is executed in the flow or rules rather than being a build + in pre-check. + + +
+ +
+ <literal>validateDTD</literal> + + Description: This operator + requires request body to be processed as XML. + + Example: + + SecDefaultAction log,deny,status:403,phase:2 +SecRule REQUEST_HEADERS:Content-Type ^text/xml$ phase:1,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML +SecRule REQBODY_PROCESSOR "!^XML$" nolog,pass,skip:1 +SecRule XML "@validateDTD /path/to/apache2/conf/xml.dtd" +
+ +
+ <literal>validateSchema</literal> + + Description: This operator + requires request body to be processed as XML. + + Example: + + SecDefaultAction log,deny,status:403,phase:2 +SecRule REQUEST_HEADERS:Content-Type ^text/xml$ phase:1,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML +SecRule REQBODY_PROCESSOR "!^XML$" nolog,pass,skip:1 +SecRule XML "@validateSchema /path/to/apache2/conf/xml.xsd" + + This operator requires request body to be processed as XML. +
+ +
+ <literal>validateUrlEncoding</literal> + + Description: Verifies the + encodings used in the variable (if any) are valid. + + Example: + + SecRule ARGS "@validateUrlEncoding" + + Note + + URL encoding is an HTTP standard for encoding byte values within a + URL. The byte is escaped with a % followed by two hexadecimal values + (0-F). This directive does not check encoding in a POST payload when the + multipart/form-data encoding (file upload) is used. It is not necessary + to do so because URL encoding is not used for this encoding. +
+ +
+ <literal>validateUtf8Encoding</literal> + + Description: Verifies the + variable is a valid UTF-8 encoded string. + + Example: + + SecRule ARGS "@validateUtf8Encoding" + + Note + + UTF-8 encoding is valid on most web servers. Integer values + between 0-65535 are encoded in a UTF-8 byte sequence that is escaped by + percents. The short form is two bytes in length. + + check for three types of errors: + + + + Not enough bytes. UTF-8 supports two, three, four, five, and + six byte encodings. ModSecurity will locate cases when a byte or + more is missing. + + + + Invalid encoding. The two most significant bits in most + characters are supposed to be fixed to 0x80. Attackers can use this + to subvert Unicode decoders. + + + + Overlong characters. ASCII characters are mapped directly into + the Unicode space and are thus represented with a single byte. + However, most ASCII characters can also be encoded with two, three, + four, five, and six characters thus tricking the decoder into + thinking that the character is something else (and, presumably, + avoiding the security check). + + +
+
+
\ No newline at end of file diff --git a/doc/pdf.xsl b/doc/pdf.xsl new file mode 100644 index 00000000..af720411 --- /dev/null +++ b/doc/pdf.xsl @@ -0,0 +1,165 @@ + + + + + + + + + + +0pt +11 +1.5 +1 20 1 + + + 0.0em + 0.0em + 0.0em + + 0.2em + 0.2em + 0.2em + + + + + + + + bold + always + left + 1.0em + 1.0em + 1.0em + 0.0em + 0.0em + 0.0em + + + + 24pt + + page + + 0.0em + 0.0em + 0.0em + + + + + + 18pt + + + + 14pt + + + + 12pt + + + + 0.0em + 0.0em + 0.0em + 0.2em + 0.2em + 0.2em + + + + 0.2em + 0.2em + 0.2em + + 0.2em + 0.2em + 0.2em + + 2pc + + + + + #f0f0f0 + 2pt + + + + 90% + + + + + + + + + + + + + + + + + + + + + + + + ​ + + + + + diff --git a/modsecurity.conf-minimal b/modsecurity.conf-minimal new file mode 100644 index 00000000..079820e3 --- /dev/null +++ b/modsecurity.conf-minimal @@ -0,0 +1,32 @@ + +# Basic configuration options +SecRuleEngine On +SecRequestBodyAccess On +SecResponseBodyAccess Off + +# Handling of file uploads +# TODO Choose a folder private to Apache. +# SecUploadDir /opt/apache-frontend/tmp/ +SecUploadKeepFiles Off + +# Debug log +SecDebugLog logs/modsec_debug.log +SecDebugLogLevel 0 + +# Serial audit log +SecAuditEngine RelevantOnly +SecAuditLogRelevantStatus ^5 +SecAuditLogParts ABIFHZ +SecAuditLogType Serial +SecAuditLog logs/modsec_audit.log + +# Maximum request body size we will +# accept for buffering +SecRequestBodyLimit 131072 + +# Store up to 128 KB in memory +SecRequestBodyInMemoryLimit 131072 + +# Buffer response bodies of up to +# 512 KB in length +SecResponseBodyLimit 524288