commit 3f80fdac3b2f7e4f10323ed70118f4b05e6682a0 Author: ivanr Date: Tue Feb 6 12:29:22 2007 +0000 Import ModSecurity 2.1.0-rc7 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 00000000..390c6493 Binary files /dev/null and b/doc/apache_request_cycle-modsecurity.jpg differ diff --git a/doc/breach-logo-small.gif b/doc/breach-logo-small.gif new file mode 100644 index 00000000..d732ac50 Binary files /dev/null and b/doc/breach-logo-small.gif differ 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 00000000..7fa98c5a Binary files /dev/null and b/doc/modsecurity-logo.png differ 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 00000000..76f5ca16 Binary files /dev/null and b/doc/modsecurity.gif differ diff --git a/doc/modsecurity2-apache-reference.xml b/doc/modsecurity2-apache-reference.xml new file mode 100644 index 00000000..eed3fc55 --- /dev/null +++ b/doc/modsecurity2-apache-reference.xml @@ -0,0 +1,4134 @@ + +
+ 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