001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.utils; 019 020import com.fasterxml.jackson.core.JsonProcessingException; 021import io.stallion.Context; 022import io.stallion.dataAccess.Model; 023import io.stallion.services.Log; 024import io.stallion.users.Role; 025import io.stallion.utils.json.JSON; 026import io.stallion.utils.json.RestrictedViews; 027import org.apache.commons.lang3.StringEscapeUtils; 028import org.apache.commons.lang3.exception.ExceptionUtils; 029import org.owasp.html.HtmlPolicyBuilder; 030import org.owasp.html.PolicyFactory; 031 032 033import java.util.regex.Pattern; 034 035 036public class Sanitize { 037 038 public static final PolicyFactory 039 COMMENTS_BOX_POLICY = new HtmlPolicyBuilder() 040 .allowStandardUrlProtocols() 041 // Allow title="..." on any element. 042 .allowAttributes("title").globally() 043 // Allow href="..." on <a> elements. 044 .allowAttributes("href", "data-mentioned-contact").onElements("a") 045 // Defeat link spammers. 046 .requireRelNofollowOnLinks() 047 .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}")) 048 .globally() 049 // The align attribute on <p> elements can have any value below. 050 .allowAttributes("align") 051 .matching(true, "center", "left", "right", "justify", "char") 052 .onElements("p") 053 // These elements are allowed. 054 .allowElements( 055 "a", "p", "i", "b", "em", "blockquote", "code", "strong", 056 "br", "ul", "ol", "li") 057 // Custom slashdot tags. 058 // These could be rewritten in the sanitizer using an ElementPolicy. 059 .allowElements("quote", "ecode") 060 061 .toFactory(); 062 063 064 public static final PolicyFactory 065 STANDARD_POLICY = new HtmlPolicyBuilder() 066 .allowTextIn("div") 067 .allowStandardUrlProtocols() 068 // Allow title="..." on any element. 069 .allowAttributes("title").globally() 070 // Allow href="..." on <a> elements. 071 .allowAttributes("href", "data-mentioned-contact").onElements("a") 072 // Defeat link spammers. 073 .requireRelNofollowOnLinks() 074 // Allow lang= with an alphabetic value on any element. 075 .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}")) 076 .globally() 077 // The align attribute on <p> elements can have any value below. 078 .allowAttributes("align") 079 .matching(true, "center", "left", "right", "justify", "char") 080 .onElements("p") 081 // These elements are allowed. 082 .allowElements( 083 "a", "p", "div", "i", "b", "em", "blockquote", "tt", "strong", 084 "br", "ul", "ol", "li") 085 // Custom slashdot tags. 086 // These could be rewritten in the sanitizer using an ElementPolicy. 087 .allowElements("quote", "ecode") 088 089 .toFactory(); 090 091 public static final PolicyFactory 092 STANDARD_POLICY_WITH_IMAGES = new HtmlPolicyBuilder() 093 094 .allowStandardUrlProtocols() 095 // Allow title="..." on any element. 096 .allowAttributes("title").globally() 097 // Allow href="..." on <a> elements. 098 .allowAttributes("href", "data-mentioned-contact").onElements("a") 099 // Defeat link spammers. 100 .requireRelNofollowOnLinks() 101 // Allow lang= with an alphabetic value on any element. 102 .allowAttributes("lang").matching(Pattern.compile("[a-zA-Z]{2,20}")) 103 .globally() 104 // The align attribute on <p> elements can have any value below. 105 .allowAttributes("align") 106 .matching(true, "center", "left", "right", "justify", "char") 107 .onElements("p") 108 // These elements are allowed. 109 .allowElements( 110 "a", "p", "div", "i", "b", "em", "blockquote", "tt", "strong", 111 "br", "ul", "ol", "li", "img") 112 // Custom slashdot tags. 113 // These could be rewritten in the sanitizer using an ElementPolicy. 114 .allowAttributes("src", "alt").onElements("img") 115 .allowElements("quote", "ecode") 116 .toFactory(); 117 118 public static final PolicyFactory BLOCK_ALL_POLICY = new HtmlPolicyBuilder() 119 .allowElements() 120 .toFactory(); 121 122 123 public static Pattern stripTagsPattern = Pattern.compile("<[^>]*>"); 124 /** Strips all HTML */ 125 public static String stripAll(String s) { 126 if (s == null) { 127 return ""; 128 } 129 return stripTagsPattern.matcher(s).replaceAll(""); 130 } 131 132 /** Standard policy for a blog comment box. Mostly the same as the standard policy, except stripts out divs too. 133 * 134 * 135 * @param s 136 * @return 137 */ 138 public static String commentSanitize(String s) { 139 if (s == null) { 140 return ""; 141 } 142 return COMMENTS_BOX_POLICY.sanitize(s); 143 } 144 145 146 /** Strips all dangerous javascript, all block HTML that could ruin the page 147 * Allows only a limited white list of tags 148 * @param s 149 * @return 150 */ 151 public static String basicSanitize(String s) { 152 if (s == null) { 153 return ""; 154 } 155 return STANDARD_POLICY.sanitize(s); 156 } 157 158 159 public static String basicSanitizeWithImages(String s) { 160 if (s == null) { 161 return ""; 162 } 163 return STANDARD_POLICY_WITH_IMAGES.sanitize(s); 164 } 165 166 public static String escapeHtmlAttribute(String s) { 167 return s.replace("\"", """).replace("'", "'").replace("<", "<").replace(">", ">"); 168 } 169 170 public static String escapeXml(String s) { 171 return StringEscapeUtils.escapeXml11(s); 172 } 173 174 175 public static Object htmlSafeJson(Object obj) { 176 177 String out = JSON.stringify(obj); 178 out = out.replace("<", "\\u003c"); 179 return out; 180 } 181 182 /** 183 * Gets the object in a JSON form that is safe for being outputted on a web page: 184 * <script> 185 * var myObj = {{ utils.htmlSafeJson(obj, "member") }} 186 * </script> 187 * @param obj 188 * @param restrictionLevel - Uses the JsonView annotation to determine which properties of the object 189 * should be outputed. Possible values are: unrestricted/public/member/owner/internal 190 * @return 191 */ 192 public static Object htmlSafeJson(Object obj, String restrictionLevel) { 193 String out = ""; 194 try { 195 restrictionLevel = restrictionLevel == null ? "public" : restrictionLevel.toLowerCase(); 196 if ("public".equals(restrictionLevel)) { 197 out = JSON.stringify(obj, RestrictedViews.Public.class, true); 198 } else if ("unrestricted".equals(restrictionLevel)) { 199 out = JSON.stringify(obj, RestrictedViews.Unrestricted.class, false); 200 } else if ("member".equals(restrictionLevel)) { 201 out = JSON.stringify(obj, RestrictedViews.Member.class, true); 202 } else if ("owner".equals(restrictionLevel)) { 203 out = JSON.stringify(obj, RestrictedViews.Owner.class, true); 204 } else if ("internal".equals(restrictionLevel)) { 205 out = JSON.stringify(obj, RestrictedViews.Internal.class, true); 206 } else { 207 out = "Unknown restriction level: " + restrictionLevel; 208 } 209 } catch (JsonProcessingException ex) { 210 String objId = obj.toString(); 211 if (obj instanceof Model) { 212 objId = ((Model)obj).getId().toString(); 213 } 214 String msg = "Error JSON.stringifying object {0}" + obj.getClass().getSimpleName() + ":" + objId; 215 if (Context.getSettings().getDebug() || Context.getUser().isInRole(Role.ADMIN)) { 216 out = msg + "\n\nStacktrace-----\n\n" + ExceptionUtils.getStackTrace(ex); 217 } 218 Log.exception(ex, msg); 219 } 220 out = out.replace("<", "\\u003c"); 221 return out; 222 } 223}