View Javadoc
1   package com.jsql.util;
2   
3   import com.jsql.model.InjectionModel;
4   import com.jsql.view.subscriber.Seal;
5   import com.jsql.model.exception.InjectionFailureException;
6   import com.jsql.model.injection.method.AbstractMethodInjection;
7   import org.apache.commons.lang3.StringUtils;
8   import org.apache.logging.log4j.LogManager;
9   import org.apache.logging.log4j.Logger;
10  
11  import java.net.IDN;
12  import java.net.MalformedURLException;
13  import java.net.URI;
14  import java.net.URISyntaxException;
15  import java.util.AbstractMap.SimpleEntry;
16  import java.util.Arrays;
17  import java.util.List;
18  import java.util.Objects;
19  import java.util.concurrent.CopyOnWriteArrayList;
20  import java.util.regex.Matcher;
21  import java.util.regex.Pattern;
22  import java.util.stream.Collectors;
23  
24  public class ParameterUtil {
25  
26      private static final Logger LOGGER = LogManager.getRootLogger();
27  
28      /**
29       * Query string built from the URL submitted by user.
30       */
31      // Fix #95787: ConcurrentModificationException
32      private List<SimpleEntry<String, String>> listQueryString = new CopyOnWriteArrayList<>();
33  
34      /**
35       * Request submitted by user.
36       */
37      private List<SimpleEntry<String, String>> listRequest = new CopyOnWriteArrayList<>();
38  
39      /**
40       * Header submitted by user.
41       */
42      private List<SimpleEntry<String, String>> listHeader = new CopyOnWriteArrayList<>();
43  
44      private String rawRequest = StringUtils.EMPTY;
45      private String rawHeader = StringUtils.EMPTY;
46      private boolean isMultipartRequest = false;
47      private static final String FORMAT_KEY_VALUE = "%s=%s";
48  
49      // ABNF primitives defined in RFC 7230
50      private static final boolean[] TCHAR = new boolean[256];
51  
52      static {
53          char[] allowedTokenChars = (
54              "!#$%&'*+-.^_`|~0123456789" +
55              "abcdefghijklmnopqrstuvwxyz" +
56              "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
57          ).toCharArray();
58          for (char c : allowedTokenChars) {
59              ParameterUtil.TCHAR[c] = true;
60          }
61      }
62  
63      private final InjectionModel injectionModel;
64      
65      public ParameterUtil(InjectionModel injectionModel) {
66          this.injectionModel = injectionModel;
67      }
68      
69      /**
70       * Send each parameter from the GUI to the model in order to
71       * start the preparation of injection, the injection process is
72       * started in a new thread via model function inputValidation().
73       */
74      public void controlInput(
75          String urlQuery,
76          String rawRequest,
77          String rawHeader,
78          AbstractMethodInjection methodInjection,
79          String typeRequest,
80          boolean isScanning
81      ) {
82          try {
83              String urlQueryFixed = urlQuery;
84              // Keep single check
85              if (urlQueryFixed.isEmpty()) {
86                  throw new MalformedURLException("empty URL");
87              } else if (!urlQueryFixed.matches("(?i)^https?://.*")) {
88                  if (!urlQueryFixed.matches("(?i)^\\w+://.*")) {
89                      LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Undefined URL protocol, forcing to [http://]");
90                      urlQueryFixed = "http://"+ urlQueryFixed;
91                  } else {
92                      throw new MalformedURLException("unknown URL protocol");
93                  }
94              }
95  
96              int port = URI.create(urlQueryFixed).getPort();
97              if (port > 65535) {  // Fix #96227: IllegalArgumentException on checkConnectionResponse()
98                  throw new MalformedURLException("port must be 65535 or lower");
99              }
100             String authority = URI.create(urlQueryFixed).getAuthority();
101             if (authority == null) {
102                 throw new MalformedURLException("undefined domain authority");
103             }
104             String authorityPunycode = IDN.toASCII(authority);
105             if (!authority.equals(authorityPunycode)) {
106                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Punycode domain detected, using [{}] instead of [{}]", authorityPunycode, authority);
107                 urlQueryFixed = urlQueryFixed.replace(authority, authorityPunycode);
108             }
109 
110             this.initQueryString(urlQueryFixed);
111             this.initHeader(rawHeader);
112             this.initRequest(rawRequest);
113 
114             this.injectionModel.getMediatorUtils().connectionUtil().setMethodInjection(methodInjection);
115             this.injectionModel.getMediatorUtils().connectionUtil().setTypeRequest(typeRequest);
116             
117             if (isScanning) {
118                 this.injectionModel.beginInjection();
119             } else {
120                 new Thread(this.injectionModel::beginInjection, "ThreadBeginInjection").start();  // in thread
121             }
122         } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
123             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect Url: {}", e.getMessage());
124             
125             // Incorrect URL, reset the start button
126             this.injectionModel.sendToViews(new Seal.EndPreparation());
127         }
128     }
129 
130     /**
131      * Check integrity of parameters defined by user.
132      * @throws InjectionFailureException when params integrity is failure
133      */
134     public void checkParametersFormat() throws InjectionFailureException {
135         this.checkOneOrLessStar();
136         this.checkStarMatchMethod();
137         this.checkMethodNotEmpty();
138         this.checkMultipart();
139         if (ParameterUtil.isInvalidName(this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest())) {
140             throw new InjectionFailureException(String.format(
141                 "Illegal method: \"%s\"",
142                 this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest()
143             ));
144         }
145     }
146 
147     /*
148      * Validates an RFC 7230 field-name.
149      */
150     public static boolean isInvalidName(String token) {
151         for (int i = 0 ; i < token.length() ; i++) {
152             char c = token.charAt(i);
153             if (c > 255 || !ParameterUtil.TCHAR[c]) {
154                 return true;
155             }
156         }
157         return token.isEmpty();
158     }
159 
160     private void checkMultipart() throws InjectionFailureException {
161         this.isMultipartRequest = false;
162 
163         if (
164             this.getListHeader()
165             .stream()
166             .filter(entry -> "Content-Type".equals(entry.getKey()))
167             .anyMatch(entry ->
168                 entry.getValue() != null
169                 && entry.getValue().contains("multipart/form-data")
170                 && entry.getValue().contains("boundary=")
171             )
172         ) {
173             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Multipart boundary found in header");
174             Matcher matcherBoundary = Pattern.compile("boundary=([^;]*)").matcher(this.getHeaderFromEntries());
175             if (matcherBoundary.find()) {
176                 String boundary = matcherBoundary.group(1);
177                 if (!this.rawRequest.contains(boundary)) {
178                     throw new InjectionFailureException(
179                         String.format("Incorrect multipart data, boundary not found in body: %s", boundary)
180                     );
181                 } else {
182                     this.isMultipartRequest = true;
183                 }
184             }
185         }
186     }
187 
188     private void checkOneOrLessStar() throws InjectionFailureException {
189         var nbStarInParameter = 0;
190         
191         if (this.getQueryStringFromEntries().contains(InjectionModel.STAR)) {
192             nbStarInParameter++;
193         }
194         if (this.getRequestFromEntries().contains(InjectionModel.STAR)) {
195             nbStarInParameter++;
196         }
197         if (this.getHeaderFromEntries().contains(InjectionModel.STAR)) {
198             nbStarInParameter++;
199         }
200         
201         // Injection Point
202         if (
203             nbStarInParameter > 1
204             || StringUtils.countMatches(this.getQueryStringFromEntries(), "*") > 1
205             || StringUtils.countMatches(this.getRequestFromEntries(), "*") > 1
206             || StringUtils.countMatches(this.getHeaderFromEntries(), "*") > 1
207         ) {
208             throw new InjectionFailureException("Character insertion [*] must be used once in Query String, Request or Header parameters");
209         }
210     }
211     
212     public void checkStarMatchMethod() throws InjectionFailureException {
213         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
214         boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().preferencesUtil().isCheckingAllParam();
215 
216         if (
217             this.getQueryStringFromEntries().contains(InjectionModel.STAR)
218             && methodInjection != this.injectionModel.getMediatorMethod().getQuery()
219             && !isCheckingAllParam
220         ) {
221             throw new InjectionFailureException("Select method GET to use character [*] or remove [*] from GET parameters");
222         } else if (
223             this.getRequestFromEntries().contains(InjectionModel.STAR)
224             && methodInjection != this.injectionModel.getMediatorMethod().getRequest()
225             && !isCheckingAllParam
226         ) {
227             throw new InjectionFailureException("Select a Request method (like POST) to use [*], or remove [*] from Request parameters");
228         } else if (
229             this.getHeaderFromEntries().contains(InjectionModel.STAR)
230             && methodInjection != this.injectionModel.getMediatorMethod().getHeader()
231             && !isCheckingAllParam
232         ) {
233             throw new InjectionFailureException("Select method Header to use character [*] or remove [*] from Header parameters");
234         }
235     }
236     
237     public void checkMethodNotEmpty() throws InjectionFailureException {
238         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
239         boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().preferencesUtil().isCheckingAllParam();
240         
241         if (
242             methodInjection == this.injectionModel.getMediatorMethod().getQuery()
243             && !isCheckingAllParam
244             && this.getListQueryString().isEmpty()
245             && !this.injectionModel.getMediatorUtils().connectionUtil().getUrlBase().contains(InjectionModel.STAR)
246         ) {
247             throw new InjectionFailureException("No query string");
248         } else if (
249             methodInjection == this.injectionModel.getMediatorMethod().getRequest()
250             && this.getListRequest().isEmpty()
251         ) {
252             throw new InjectionFailureException("Incorrect Request format");
253         } else if (
254             methodInjection == this.injectionModel.getMediatorMethod().getHeader()
255             && this.getListHeader().isEmpty()
256         ) {
257             throw new InjectionFailureException("Incorrect Header format");
258         }
259     }
260     
261     public String initStar(SimpleEntry<String, String> parameterToInject) {
262         String characterInsertionByUser;
263         if (parameterToInject == null) {
264             characterInsertionByUser = InjectionModel.STAR;
265         } else {
266             characterInsertionByUser = parameterToInject.getValue();
267             parameterToInject.setValue(InjectionModel.STAR);
268         }
269         return characterInsertionByUser;
270     }
271 
272     public void initQueryString(String urlQuery) throws MalformedURLException, URISyntaxException {
273         // Format and get rid of anchor fragment using native URL
274         var url = new URI(urlQuery).toURL();
275         
276         if (
277             StringUtils.isEmpty(urlQuery)
278             || StringUtils.isEmpty(url.getHost())
279         ) {
280             throw new MalformedURLException("empty URL");
281         }
282         
283         this.injectionModel.getMediatorUtils().connectionUtil().setUrlByUser(urlQuery);
284         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(urlQuery);
285         this.listQueryString.clear();
286         
287         // Parse url and GET query string
288         var regexQueryString = Pattern.compile("(.*\\?)(.*)").matcher(urlQuery);
289         if (!regexQueryString.find()) {
290             return;
291         }
292         
293         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(regexQueryString.group(1));
294         
295         if (StringUtils.isNotEmpty(url.getQuery())) {
296             this.listQueryString = Pattern.compile("&")
297                 .splitAsStream(url.getQuery())
298                 .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
299                 .map(keyValue -> new SimpleEntry<>(
300                     keyValue[0],
301                     keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
302                 )).collect(Collectors.toCollection(CopyOnWriteArrayList::new));  // Fix #96224: ConcurrentModificationException
303         }
304     }
305 
306     public void initRequest(String rawRequest) {
307         this.rawRequest = rawRequest;
308         this.listRequest.clear();
309         if (StringUtils.isNotEmpty(rawRequest)) {
310             if (this.isMultipartRequest) {
311                 // Pass request containing star * param without any parsing
312                 this.listRequest = new CopyOnWriteArrayList<>(List.of(new SimpleEntry<>(
313                     rawRequest,
314                     StringUtils.EMPTY
315                 )));
316             } else {
317                 this.listRequest = Pattern.compile("&")
318                     .splitAsStream(rawRequest)
319                     .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
320                     .map(keyValue -> new SimpleEntry<>(
321                         keyValue[0],
322                         keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
323                     )).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
324             }
325         }
326     }
327 
328     public void initHeader(String rawHeader) {
329         this.rawHeader = rawHeader;
330         this.listHeader.clear();
331         if (StringUtils.isNotEmpty(rawHeader)) {
332             this.listHeader = Pattern.compile("\\\\r\\\\n")
333                 .splitAsStream(rawHeader)
334                 .map(keyValue -> Arrays.copyOf(keyValue.split(":"), 2))
335                 .map(keyValue -> new SimpleEntry<>(
336                     keyValue[0],
337                     keyValue[1] == null ? StringUtils.EMPTY : keyValue[1]
338                 )).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
339         }
340     }
341     
342     public String getQueryStringFromEntries() {
343         return this.listQueryString.stream()
344             .filter(Objects::nonNull)
345             .map(entry -> {
346                 if (
347                     this.injectionModel.getMediatorStrategy().getStrategy() == this.injectionModel.getMediatorStrategy().getMultibit()
348                     && entry.getValue() != null
349                     && entry.getValue().contains(InjectionModel.STAR)
350                 ) {
351                     return String.format(ParameterUtil.FORMAT_KEY_VALUE, entry.getKey(), InjectionModel.STAR);
352                 } else {
353                     return String.format(ParameterUtil.FORMAT_KEY_VALUE, entry.getKey(), entry.getValue());
354                 }
355             })
356             .collect(Collectors.joining("&"));
357     }
358 
359     public String getRequestFromEntries() {
360         return this.listRequest.stream()
361             .filter(Objects::nonNull)
362             .map(entry -> String.format(
363                 ParameterUtil.FORMAT_KEY_VALUE,
364                 entry.getKey(),
365                 StringUtils.isEmpty(entry.getValue()) ? StringUtils.EMPTY : entry.getValue()
366             ))
367             .collect(Collectors.joining("&"));
368     }
369     
370     public String getHeaderFromEntries() {
371         return this.listHeader.stream()
372             .filter(Objects::nonNull)
373             .map(entry -> String.format("%s:%s", entry.getKey(), entry.getValue()))
374             .collect(Collectors.joining("\\r\\n"));
375     }
376 
377     public boolean isRequestSoap() {
378         return this.rawRequest
379             .trim()
380             .matches("^(<soapenv:|<\\?xml).*");
381     }
382 
383     
384     // Getters / setters
385     
386     public String getRawRequest() {
387         return this.rawRequest;
388     }
389 
390     public String getRawHeader() {
391         return this.rawHeader;
392     }
393 
394     public List<SimpleEntry<String, String>> getListRequest() {
395         return this.listRequest;
396     }
397 
398     public List<SimpleEntry<String, String>> getListHeader() {
399         return this.listHeader;
400     }
401 
402     public List<SimpleEntry<String, String>> getListQueryString() {
403         return this.listQueryString;
404     }
405     
406     public boolean isMultipartRequest() {
407         return this.isMultipartRequest;
408     }
409 }