001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.markup.html; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.concurrent.ConcurrentHashMap; 022import java.util.concurrent.ConcurrentLinkedQueue; 023import java.util.concurrent.ConcurrentMap; 024import java.util.regex.Pattern; 025 026import org.apache.wicket.util.collections.ReverseListIterator; 027import org.apache.wicket.util.string.Strings; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031 032/** 033 * This is a resource guard which by default denies access to all resources and thus is more secure. 034 * <p/> 035 * All pattern are executed in the order they were provided. All pattern are executed to determine 036 * if access can be granted or not. 037 * <p/> 038 * Note that access to the config data such as get/setPattern() and acceptXXX() is not synchronized. 039 * It is assumed that configuration has finished before the first request gets executed. 040 * <p/> 041 * The rules are fairly simple. Each pattern must start with either "+" (include) or "-" (exclude). 042 * "*" is a placeholder for zero, one or more characters within a file or directory name. "**" is a 043 * placeholder for zero, one or more sub-directories. 044 * <p/> 045 * Examples: 046 * <table border="0"> 047 * <caption>Examples</caption> 048 * <tr> 049 * <td>+*.gif</td> 050 * <td>All gif files in all directories</td> 051 * </tr> 052 * <tr> 053 * <td>+test*.*</td> 054 * <td>All files in all directories starting with "test"</td> 055 * </tr> 056 * <tr> 057 * <td>+mydir/*/*.gif</td> 058 * <td>All gif files two levels below the mydir directory. E.g. mydir/dir2/test.gif</td> 059 * </tr> 060 * <tr> 061 * <td>+mydir/**/*.gif</td> 062 * <td>All gif files in all directories below mydir. E.g. mydir/test.gif or 063 * mydir/dir2/dir3/test.gif</td> 064 * </tr> 065 * </table> 066 * 067 * @see IPackageResourceGuard 068 * @see org.apache.wicket.settings.ResourceSettings#getPackageResourceGuard 069 * @see PackageResourceGuard 070 * 071 * @author Juergen Donnerstag 072 */ 073public class SecurePackageResourceGuard extends PackageResourceGuard 074{ 075 /** Log. */ 076 private static final Logger log = LoggerFactory.getLogger(SecurePackageResourceGuard.class); 077 078 /** The path separator used */ 079 private static final char PATH_SEPARATOR = '/'; 080 081 /** The list of pattern. Note that the order is important, hence a list */ 082 private List<SearchPattern> pattern = new ArrayList<>(); 083 084 /** A cache to speed up the checks */ 085 private final ConcurrentMap<String, Boolean> cache; 086 087 /** 088 * Constructor. 089 */ 090 public SecurePackageResourceGuard() 091 { 092 this(new SimpleCache(100)); 093 } 094 095 /** 096 * Constructor. 097 * 098 * @param cache 099 * the internal cache that will hold the results for all already checked resources. 100 * Use {@code null} to disable caching. 101 */ 102 public SecurePackageResourceGuard(final ConcurrentMap<String, Boolean> cache) 103 { 104 this.cache = cache; 105 106 // the order is important for better performance 107 // first add the most commonly used 108 addPattern("+*.js"); 109 addPattern("+*.css"); 110 addPattern("+*.png"); 111 addPattern("+*.jpg"); 112 addPattern("+*.jpeg"); 113 addPattern("+*.gif"); 114 addPattern("+*.ico"); 115 addPattern("+*.cur"); 116 addPattern("+*.map"); 117 118 // WICKET-208 non page templates may be served 119 addPattern("+*.html"); 120 121 addPattern("+*.txt"); 122 addPattern("+*.swf"); 123 addPattern("+*.bmp"); 124 addPattern("+*.svg"); 125 addPattern("+*.avif"); 126 127 // allow web fonts 128 addPattern("+*.eot"); 129 addPattern("+*.ttf"); 130 addPattern("+*.woff"); 131 addPattern("+*.woff2"); 132 133 } 134 135 /** 136 * 137 */ 138 public void clearCache() 139 { 140 if (cache != null) 141 { 142 cache.clear(); 143 } 144 } 145 146 /** 147 * Whether the provided absolute path is accepted. 148 * 149 * @param path 150 * The absolute path, starting from the class root (packages are separated with 151 * forward slashes instead of dots). 152 * @return True if accepted, false otherwise. 153 */ 154 @Override 155 public boolean accept(String path) 156 { 157 // First check the cache 158 if (cache != null) 159 { 160 Boolean rtn = cache.get(path); 161 if (rtn != null) 162 { 163 return rtn; 164 } 165 } 166 167 // Check typical files such as log4j.xml etc. 168 if (super.accept(path) == false) 169 { 170 return false; 171 } 172 173 // Check against the pattern 174 boolean hit = false; 175 for (SearchPattern pattern : new ReverseListIterator<>(this.pattern)) 176 { 177 if ((pattern != null) && pattern.isActive()) 178 { 179 if (pattern.matches(path)) 180 { 181 hit = pattern.isInclude(); 182 break; 183 } 184 } 185 } 186 187 if (cache != null) 188 { 189 // Do not use putIfAbsent(). See newCache() 190 cache.put(path, (hit ? Boolean.TRUE : Boolean.FALSE)); 191 } 192 193 if (hit == false) 194 { 195 log.warn("Access denied to shared (static) resource: " + path); 196 } 197 198 return hit; 199 } 200 201 /** 202 * Gets the current list of pattern. Please invoke clearCache() or setPattern(List) when 203 * finished in order to clear the cache of previous checks. 204 * 205 * @return pattern 206 */ 207 public List<SearchPattern> getPattern() 208 { 209 clearCache(); 210 return pattern; 211 } 212 213 /** 214 * Sets pattern. 215 * 216 * @param pattern 217 * pattern 218 */ 219 public void setPattern(List<SearchPattern> pattern) 220 { 221 this.pattern = pattern; 222 clearCache(); 223 } 224 225 /** 226 * @param pattern 227 */ 228 public void addPattern(String pattern) 229 { 230 this.pattern.add(new SearchPattern(pattern)); 231 clearCache(); 232 } 233 234 /** 235 * 236 */ 237 public static class SearchPattern 238 { 239 private String pattern; 240 241 private Pattern regex; 242 243 private boolean include; 244 245 private boolean active = true; 246 247 private boolean fileOnly; 248 249 /** 250 * Construct. 251 * 252 * @param pattern 253 */ 254 public SearchPattern(final String pattern) 255 { 256 setPattern(pattern); 257 } 258 259 /** 260 * 261 * @param pattern 262 * @return Regex pattern 263 */ 264 private Pattern convertToRegex(final String pattern) 265 { 266 String regex = Strings.replaceAll(pattern, ".", "#dot#").toString(); 267 268 // If path starts with "*/" or "**/" 269 regex = regex.replaceAll("^\\*" + PATH_SEPARATOR, "[^" + PATH_SEPARATOR + "]+" + 270 PATH_SEPARATOR); 271 regex = regex.replaceAll("^[\\*]{2,}" + PATH_SEPARATOR, "([^" + PATH_SEPARATOR + 272 "].#star#" + PATH_SEPARATOR + ")?"); 273 274 // Handle "/*/" and "/**/" 275 regex = regex.replaceAll(PATH_SEPARATOR + "\\*" + PATH_SEPARATOR, PATH_SEPARATOR + 276 "[^" + PATH_SEPARATOR + "]+" + PATH_SEPARATOR); 277 regex = regex.replaceAll(PATH_SEPARATOR + "[\\*]{2,}" + PATH_SEPARATOR, "(" + 278 PATH_SEPARATOR + "|" + PATH_SEPARATOR + ".+" + PATH_SEPARATOR + ")"); 279 280 // Handle "*" within dir or file names 281 regex = regex.replaceAll("\\*+", "[^" + PATH_SEPARATOR + "]*"); 282 283 // replace placeholder 284 regex = Strings.replaceAll(regex, "#dot#", "\\.").toString(); 285 regex = Strings.replaceAll(regex, "#star#", "*").toString(); 286 287 return Pattern.compile(regex); 288 } 289 290 /** 291 * Gets pattern. 292 * 293 * @return pattern 294 */ 295 public String getPattern() 296 { 297 return pattern; 298 } 299 300 /** 301 * Gets regex. 302 * 303 * @return regex 304 */ 305 public Pattern getRegex() 306 { 307 return regex; 308 } 309 310 /** 311 * Sets pattern. 312 * 313 * @param pattern 314 * pattern 315 */ 316 public void setPattern(String pattern) 317 { 318 if (Strings.isEmpty(pattern)) 319 { 320 throw new IllegalArgumentException( 321 "Parameter 'pattern' can not be null or an empty string"); 322 } 323 324 if (pattern.charAt(0) == '+') 325 { 326 include = true; 327 } 328 else if (pattern.charAt(0) == '-') 329 { 330 include = false; 331 } 332 else 333 { 334 throw new IllegalArgumentException( 335 "Parameter 'pattern' must start with either '+' or '-'. pattern='" + pattern + 336 "'"); 337 } 338 339 this.pattern = pattern; 340 regex = convertToRegex(pattern.substring(1)); 341 342 fileOnly = (pattern.indexOf(PATH_SEPARATOR) == -1); 343 } 344 345 /** 346 * 347 * @param path 348 * @return True if 'path' matches the pattern 349 */ 350 public boolean matches(String path) 351 { 352 if (fileOnly) 353 { 354 path = Strings.lastPathComponent(path, PATH_SEPARATOR); 355 } 356 return regex.matcher(path).matches(); 357 } 358 359 /** 360 * Gets include. 361 * 362 * @return include 363 */ 364 public boolean isInclude() 365 { 366 return include; 367 } 368 369 /** 370 * Sets include. 371 * 372 * @param include 373 * include 374 */ 375 public void setInclude(boolean include) 376 { 377 this.include = include; 378 } 379 380 /** 381 * Gets active. 382 * 383 * @return active 384 */ 385 public boolean isActive() 386 { 387 return active; 388 } 389 390 /** 391 * Sets active. 392 * 393 * @param active 394 * active 395 */ 396 public void setActive(boolean active) 397 { 398 this.active = active; 399 } 400 401 @Override 402 public String toString() 403 { 404 return "Pattern: " + pattern + ", Regex: " + regex + ", include:" + include + 405 ", fileOnly:" + fileOnly + ", active:" + active; 406 } 407 } 408 409 /** 410 * A very simple cache 411 */ 412 public static class SimpleCache extends ConcurrentHashMap<String, Boolean> 413 { 414 private static final long serialVersionUID = 1L; 415 416 private final ConcurrentLinkedQueue<String> fifo = new ConcurrentLinkedQueue<>(); 417 418 private final int maxSize; 419 420 /** 421 * Construct. 422 * 423 * @param maxSize 424 */ 425 public SimpleCache(int maxSize) 426 { 427 this.maxSize = maxSize; 428 } 429 430 /** 431 * @see java.util.concurrent.ConcurrentHashMap#put(java.lang.Object, java.lang.Object) 432 */ 433 @Override 434 public Boolean put(String key, Boolean value) 435 { 436 // add the key to the hash map. Do not replace existing once 437 Boolean rtn = super.putIfAbsent(key, value); 438 439 // If found, than remove it from the fifo list and ... 440 if (rtn != null) 441 { 442 fifo.remove(key); 443 } 444 445 // append it at the end of the list 446 fifo.add(key); 447 448 // remove all "outdated" cache entries 449 while (fifo.size() > maxSize) 450 { 451 remove(fifo.poll()); 452 } 453 return rtn; 454 } 455 } 456}