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  
48      public static final String PREFIX_COMMAND_QUERY = "Query#";
49      public static final String PREFIX_COMMAND_REQUEST = "Request#";
50      public static final String PREFIX_COMMAND_HEADER = "Header#";
51      public static final String PREFIX_COMMAND_COOKIE = "Cookie#";
52      private static final String FORMAT_KEY_VALUE = "%s=%s";
53  
54      // ABNF primitives defined in RFC 7230
55      private static final boolean[] TCHAR = new boolean[256];
56  
57      static {
58          char[] allowedTokenChars = (
59              "!#$%&'*+-.^_`|~0123456789" +
60              "abcdefghijklmnopqrstuvwxyz" +
61              "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
62          ).toCharArray();
63          for (char c : allowedTokenChars) {
64              ParameterUtil.TCHAR[c] = true;
65          }
66      }
67  
68      private final InjectionModel injectionModel;
69      
70      public ParameterUtil(InjectionModel injectionModel) {
71          this.injectionModel = injectionModel;
72      }
73      
74      /**
75       * Send each parameter from the GUI to the model in order to
76       * start the preparation of injection, the injection process is
77       * started in a new thread via model function inputValidation().
78       */
79      public void controlInput(
80          String selectionCommand,
81          String urlQuery,
82          String rawRequest,
83          String rawHeader,
84          AbstractMethodInjection methodInjection,
85          String typeRequest,
86          boolean isScanning
87      ) {
88          try {
89              String urlQueryFixed = urlQuery;
90              // Keep single check
91              if (urlQueryFixed.isEmpty()) {
92                  throw new MalformedURLException("empty URL");
93              } else if (!urlQueryFixed.matches("(?i)^https?://.*")) {
94                  if (!urlQueryFixed.matches("(?i)^\\w+://.*")) {
95                      LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Undefined URL protocol, forcing to [http://]");
96                      urlQueryFixed = "http://"+ urlQueryFixed;
97                  } else {
98                      throw new MalformedURLException("unknown URL protocol");
99                  }
100             }
101 
102             int port = URI.create(urlQueryFixed).getPort();
103             if (port > 65535) {  // Fix #96227: IllegalArgumentException on checkConnectionResponse()
104                 throw new MalformedURLException("port must be 65535 or lower");
105             }
106             String authority = URI.create(urlQueryFixed).getAuthority();
107             if (authority == null) {
108                 throw new MalformedURLException("undefined domain authority");
109             }
110             String authorityPunycode = IDN.toASCII(authority);
111             if (!authority.equals(authorityPunycode)) {
112                 LOGGER.log(LogLevelUtil.CONSOLE_INFORM, "Punycode domain detected, using [{}] instead of [{}]", authorityPunycode, authority);
113                 urlQueryFixed = urlQueryFixed.replace(authority, authorityPunycode);
114             }
115 
116             this.initQueryString(urlQueryFixed, selectionCommand);
117             this.initRequest(rawRequest, selectionCommand);
118             this.initHeader(rawHeader, selectionCommand);
119 
120             this.injectionModel.getMediatorUtils().connectionUtil().withMethodInjection(methodInjection);
121             this.injectionModel.getMediatorUtils().connectionUtil().withTypeRequest(typeRequest);
122             
123             if (isScanning) {
124                 this.injectionModel.beginInjection();
125             } else {
126                 new Thread(this.injectionModel::beginInjection, "ThreadBeginInjection").start();  // in thread
127             }
128         } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
129             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect Url: {}", e.getMessage());
130             
131             // Incorrect URL, reset the start button
132             this.injectionModel.sendToViews(new Seal.EndPreparation());
133         }
134     }
135 
136     /**
137      * Check integrity of parameters defined by user.
138      * @throws InjectionFailureException when params integrity is failure
139      */
140     public void checkParametersFormat() throws InjectionFailureException {
141         this.checkOneOrLessStar();
142         this.checkStarMatchMethod();
143         this.checkMethodNotEmpty();
144         this.checkMultipart();
145         if (ParameterUtil.isInvalidName(this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest())) {
146             throw new InjectionFailureException(String.format(
147                 "Illegal method: %s",
148                 this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest()
149             ));
150         }
151     }
152 
153     /*
154      * Validates an RFC 7230 field-name.
155      */
156     public static boolean isInvalidName(String token) {
157         for (int i = 0 ; i < token.length() ; i++) {
158             char c = token.charAt(i);
159             if (c > 255 || !ParameterUtil.TCHAR[c]) {
160                 return true;
161             }
162         }
163         return token.isEmpty();
164     }
165 
166     private void checkMultipart() throws InjectionFailureException {
167         this.isMultipartRequest = false;
168 
169         if (
170             this.getListHeader()
171             .stream()
172             .filter(entry -> "Content-Type".equals(entry.getKey()))
173             .anyMatch(entry ->
174                 entry.getValue() != null
175                 && entry.getValue().contains("multipart/form-data")
176                 && entry.getValue().contains("boundary=")
177             )
178         ) {
179             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Multipart boundary found in header");
180             Matcher matcherBoundary = Pattern.compile("boundary=([^;]*)").matcher(this.getHeaderFromEntries());
181             if (matcherBoundary.find()) {
182                 String boundary = matcherBoundary.group(1);
183                 if (!this.rawRequest.contains(boundary)) {
184                     throw new InjectionFailureException(
185                         String.format("Incorrect multipart data, boundary not found in body: %s", boundary)
186                     );
187                 } else {
188                     this.isMultipartRequest = true;
189                 }
190             }
191         }
192     }
193 
194     private void checkOneOrLessStar() throws InjectionFailureException {
195         var nbStarAcrossParameters = 0;
196         
197         if (this.getQueryStringFromEntries().contains(InjectionModel.STAR)) {
198             nbStarAcrossParameters++;
199         }
200         if (this.getRequestFromEntries().contains(InjectionModel.STAR)) {
201             nbStarAcrossParameters++;
202         }
203         if (
204             this.getHeaderFromEntries().contains(InjectionModel.STAR)
205             && this.getCountHeadersWithStar() > 1
206         ) {
207             nbStarAcrossParameters++;
208         }
209 
210         if (
211             nbStarAcrossParameters >= 2
212             || StringUtils.countMatches(this.getQueryStringFromEntries(), "*") >= 2
213             || StringUtils.countMatches(this.getRequestFromEntries(), "*") >= 2
214             || this.getCountHeadersWithStar() > 1
215         ) {
216             throw new InjectionFailureException("param selected or [*] can be only used once in URL, Request or Header");
217         }
218     }
219 
220     private long getCountHeadersWithStar() {
221         return this.getListHeader().stream()
222             .filter(s -> s.getValue().contains(InjectionModel.STAR))
223             .filter(s ->
224                 !List.of(
225                     "accept", "accept-encoding", "accept-language", "access-control-request-headers", "if-match", "if-none-match", "allow"
226                 ).contains(s.getKey().toLowerCase())
227             )
228             .count();
229     }
230 
231     public void checkStarMatchMethod() throws InjectionFailureException {
232         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
233         boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().preferencesUtil().isCheckingAllParam();
234 
235         if (
236             this.getQueryStringFromEntries().contains(InjectionModel.STAR)
237             && methodInjection != this.injectionModel.getMediatorMethod().getQuery()
238             && !isCheckingAllParam
239         ) {
240             throw new InjectionFailureException("param in URL selected but method Request or Header selected");
241         } else if (
242             this.getRequestFromEntries().contains(InjectionModel.STAR)
243             && methodInjection != this.injectionModel.getMediatorMethod().getRequest()
244             && !isCheckingAllParam
245         ) {
246             throw new InjectionFailureException("param in Request selected but method URL or Header selected");
247         } else if (
248             this.getHeaderFromEntries().contains(InjectionModel.STAR)
249             && methodInjection != this.injectionModel.getMediatorMethod().getHeader()
250             && !isCheckingAllParam
251             && this.getCountHeadersWithStar() > 0
252         ) {
253             throw new InjectionFailureException("param in Header selected but method URL or Request selected");
254         }
255     }
256     
257     public void checkMethodNotEmpty() throws InjectionFailureException {
258         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
259 
260         if (
261             methodInjection == this.injectionModel.getMediatorMethod().getQuery()
262             && this.getListQueryString().isEmpty()
263             && !this.injectionModel.getMediatorUtils().connectionUtil().getUrlBase().contains(InjectionModel.STAR)
264         ) {
265             throw new InjectionFailureException("empty URL param");
266         } else if (
267             methodInjection == this.injectionModel.getMediatorMethod().getRequest()
268             && this.getListRequest().isEmpty()
269         ) {
270             throw new InjectionFailureException("empty Request param");
271         } else if (
272             methodInjection == this.injectionModel.getMediatorMethod().getHeader()
273             && this.getListHeader().isEmpty()
274         ) {
275             throw new InjectionFailureException("empty Header param");
276         }
277     }
278     
279     public String initStar(SimpleEntry<String, String> parameterToInject) {
280         String characterInsertionByUser;
281         if (parameterToInject.getValue().contains(InjectionModel.STAR)) {  // star already set into value when checking all cookies
282             characterInsertionByUser = parameterToInject.getValue().replace(
283                 InjectionModel.STAR,
284                 InjectionModel.STAR
285                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
286             );
287         } else {
288             characterInsertionByUser = parameterToInject.getValue()
289             + (parameterToInject.getValue().matches("\\d$") ? "+" : StringUtils.EMPTY)  // required when char insertion numeric by user and not found, space not working as zipped
290             + InjectionModel.STAR
291             + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment();
292         }
293         // char insertion contains remainder payload when found
294         parameterToInject.setValue(
295             InjectionModel.STAR
296             + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
297         );
298         return characterInsertionByUser;
299     }
300 
301     public void initQueryString(String urlQuery) throws MalformedURLException, URISyntaxException {
302         this.initQueryString(urlQuery, StringUtils.EMPTY);
303     }
304 
305     public void initQueryString(String urlQuery, String selectionCommand) throws MalformedURLException, URISyntaxException {
306         // Format and get rid of anchor fragment using native URL
307         var url = new URI(urlQuery).toURL();
308         
309         if (
310             StringUtils.isEmpty(urlQuery)
311             || StringUtils.isEmpty(url.getHost())
312         ) {
313             throw new MalformedURLException("empty URL");
314         }
315         
316         this.injectionModel.getMediatorUtils().connectionUtil().setUrlByUser(urlQuery);
317         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(urlQuery);
318         this.listQueryString.clear();
319         
320         // Parse url and GET query string
321         var regexQueryString = Pattern.compile("(.*\\?)(.*)").matcher(urlQuery);
322         if (!regexQueryString.find()) {
323             return;
324         }
325         
326         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(regexQueryString.group(1));
327         
328         if (StringUtils.isNotEmpty(url.getQuery())) {
329             this.listQueryString = Pattern.compile("&")
330                 .splitAsStream(url.getQuery())
331                 .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
332                 .map(keyValue -> {
333                     var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_QUERY, StringUtils.EMPTY);
334                     return new SimpleEntry<>(
335                         keyValue[0],
336                         (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1])
337                         + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
338                     );
339                 }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));  // Fix #96224: ConcurrentModificationException
340         }
341     }
342 
343     public void initRequest(String rawRequest) {
344         this.initRequest(rawRequest, StringUtils.EMPTY);
345     }
346 
347     public void initRequest(String rawRequest, String selectionCommand) {
348         this.rawRequest = rawRequest;
349         this.listRequest.clear();
350         if (StringUtils.isNotEmpty(rawRequest)) {
351             if (this.isMultipartRequest || this.isRequestSoap()) {
352                 // Pass request containing star * param without any parsing
353                 this.listRequest = new CopyOnWriteArrayList<>(List.of(new SimpleEntry<>(
354                     rawRequest,
355                     StringUtils.EMPTY
356                 )));
357             } else {
358                 this.listRequest = Pattern.compile("&")
359                     .splitAsStream(rawRequest)
360                     .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
361                     .map(keyValue -> {
362                         var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_REQUEST, StringUtils.EMPTY);
363                         return new SimpleEntry<>(
364                             keyValue[0],
365                             (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1].replace("\\n", "\n"))
366                             + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
367                         );
368                     }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
369             }
370         }
371     }
372 
373     public void initHeader(String rawHeader) {
374         this.initHeader(rawHeader, StringUtils.EMPTY);
375     }
376 
377     public void initHeader(String rawHeader, String selectionCommand) {
378         this.rawHeader = rawHeader;
379         this.listHeader.clear();
380         if (StringUtils.isNotEmpty(rawHeader)) {
381             this.listHeader = Pattern.compile("\\\\r\\\\n")
382                 .splitAsStream(rawHeader)
383                 .map(keyValue -> Arrays.copyOf(keyValue.split(":"), 2))
384                 .map(keyValue -> {
385                     var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_HEADER, StringUtils.EMPTY);
386                     return new SimpleEntry<>(
387                         keyValue[0],
388                         (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1])
389                         + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
390                     );
391                 }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
392         }
393     }
394     
395     public String getQueryStringFromEntries() {
396         return this.listQueryString.stream()
397             .filter(Objects::nonNull)
398             .map(entry -> String.format(
399                 ParameterUtil.FORMAT_KEY_VALUE,
400                 entry.getKey(),
401                 entry.getValue())
402             )
403             .collect(Collectors.joining("&"));
404     }
405 
406     public String getRequestFromEntries() {
407         return this.listRequest.stream()
408             .filter(Objects::nonNull)
409             .map(entry -> String.format(
410                 ParameterUtil.FORMAT_KEY_VALUE,
411                 entry.getKey(),
412                 StringUtils.isEmpty(entry.getValue()) ? StringUtils.EMPTY : entry.getValue()
413             ))
414             .collect(Collectors.joining("&"));
415     }
416     
417     public String getHeaderFromEntries() {
418         return this.listHeader.stream()
419             .filter(Objects::nonNull)
420             .map(entry -> String.format("%s:%s", entry.getKey(), entry.getValue()))
421             .collect(Collectors.joining("\\r\\n"));
422     }
423 
424     public boolean isRequestSoap() {
425         return this.rawRequest.trim().matches("(?s)^\\s*(<soapenv:|<\\?xml).*");
426     }
427 
428     
429     // Getters / setters
430     
431     public String getRawRequest() {
432         return this.rawRequest;
433     }
434 
435     public String getRawHeader() {
436         return this.rawHeader;
437     }
438 
439     public List<SimpleEntry<String, String>> getListRequest() {
440         return this.listRequest;
441     }
442 
443     public List<SimpleEntry<String, String>> getListHeader() {
444         return this.listHeader;
445     }
446 
447     public List<SimpleEntry<String, String>> getListQueryString() {
448         return this.listQueryString;
449     }
450     
451     public boolean isMultipartRequest() {
452         return this.isMultipartRequest;
453     }
454 }