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