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  // todo should be managed at parent level
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             if (
116                 this.injectionModel.getMediatorUtils().preferencesUtil().isProcessingCsrf()
117                 && this.injectionModel.getMediatorUtils().preferencesUtil().isCsrfUserTag()
118                 && (
119                     StringUtils.isBlank(this.injectionModel.getMediatorUtils().preferencesUtil().csrfUserTag())
120                     || StringUtils.isBlank(this.injectionModel.getMediatorUtils().preferencesUtil().csrfUserTagOutput())
121                 )
122             ) {
123                 throw new IllegalArgumentException("undefined input or output custom CSRF token");
124             }
125 
126             this.initQueryString(urlQueryFixed, selectionCommand);
127             this.initRequest(rawRequest, selectionCommand);
128             this.initHeader(rawHeader, selectionCommand);
129 
130             this.injectionModel.getMediatorUtils().connectionUtil().withMethodInjection(methodInjection);
131             this.injectionModel.getMediatorUtils().connectionUtil().withTypeRequest(typeRequest);
132 
133             this.injectionModel.setIsInjectingWithoutScan(!isScanning);
134             if (isScanning) {  // TODO should manage thread at same level for scan and non-scan
135                 this.injectionModel.beginInjection();
136             } else {
137                 new Thread(this.injectionModel::beginInjection, "ThreadBeginInjection").start();  // in thread
138             }
139         } catch (IllegalArgumentException | MalformedURLException | URISyntaxException e) {
140             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, "Incorrect URL or setting: {}", e.getMessage());
141             
142             // Incorrect URL, reset the start button
143             this.injectionModel.sendToViews(new Seal.EndPreparation());
144         }
145     }
146 
147     /**
148      * Check integrity of parameters defined by user.
149      * @throws InjectionFailureException when params integrity is failure
150      */
151     public void checkParametersFormat() throws InjectionFailureException {
152         this.checkOneOrLessStar();
153         this.checkStarMatchMethod();
154         this.checkMethodNotEmpty();
155         this.checkMultipart();
156         if (ParameterUtil.isInvalidName(this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest())) {
157             throw new InjectionFailureException(String.format(
158                 "Illegal method: %s",
159                 this.injectionModel.getMediatorUtils().connectionUtil().getTypeRequest()
160             ));
161         }
162     }
163 
164     /*
165      * Validates an RFC 7230 field-name.
166      */
167     public static boolean isInvalidName(String token) {
168         for (int i = 0 ; i < token.length() ; i++) {
169             char c = token.charAt(i);
170             if (c > 255 || !ParameterUtil.TCHAR[c]) {
171                 return true;
172             }
173         }
174         return token.isEmpty();
175     }
176 
177     private void checkMultipart() throws InjectionFailureException {
178         this.isMultipartRequest = false;
179 
180         if (
181             this.getListHeader()
182             .stream()
183             .filter(entry -> "Content-Type".equals(entry.getKey()))
184             .anyMatch(entry ->
185                 entry.getValue() != null
186                 && entry.getValue().contains("multipart/form-data")
187                 && entry.getValue().contains("boundary=")
188             )
189         ) {
190             LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, "Multipart boundary found in header");
191             Matcher matcherBoundary = Pattern.compile("boundary=([^;]*)").matcher(this.getHeaderFromEntries());
192             if (matcherBoundary.find()) {
193                 String boundary = matcherBoundary.group(1);
194                 if (!this.rawRequest.contains(boundary)) {
195                     throw new InjectionFailureException(
196                         String.format("Incorrect multipart data, boundary not found in body: %s", boundary)
197                     );
198                 } else {
199                     this.isMultipartRequest = true;
200                 }
201             }
202         }
203     }
204 
205     private void checkOneOrLessStar() throws InjectionFailureException {
206         var nbStarAcrossParameters = 0;
207         
208         if (this.getQueryStringFromEntries().contains(InjectionModel.STAR)) {
209             nbStarAcrossParameters++;
210         }
211         if (this.getRequestFromEntries().contains(InjectionModel.STAR)) {
212             nbStarAcrossParameters++;
213         }
214         if (
215             this.getHeaderFromEntries().contains(InjectionModel.STAR)
216             && this.getCountHeadersWithStar() > 1
217         ) {
218             nbStarAcrossParameters++;
219         }
220 
221         if (
222             nbStarAcrossParameters >= 2
223             || StringUtils.countMatches(this.getQueryStringFromEntries(), "*") >= 2
224             || StringUtils.countMatches(this.getRequestFromEntries(), "*") >= 2
225             || this.getCountHeadersWithStar() > 1
226         ) {
227             throw new InjectionFailureException("param selected or [*] can be only used once in URL, Request or Header");
228         }
229     }
230 
231     private long getCountHeadersWithStar() {
232         return this.getListHeader().stream()
233             .filter(s -> s.getValue().contains(InjectionModel.STAR))
234             .filter(s ->
235                 !List.of(
236                     "accept", "accept-encoding", "accept-language", "access-control-request-headers", "if-match", "if-none-match", "allow"
237                 ).contains(s.getKey().toLowerCase())
238             )
239             .count();
240     }
241 
242     public void checkStarMatchMethod() throws InjectionFailureException {
243         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
244         boolean isCheckingAllParam = this.injectionModel.getMediatorUtils().preferencesUtil().isCheckingAllParam();
245 
246         if (
247             this.getQueryStringFromEntries().contains(InjectionModel.STAR)
248             && methodInjection != this.injectionModel.getMediatorMethod().getQuery()
249             && !isCheckingAllParam
250         ) {
251             throw new InjectionFailureException("param in URL selected but method Request or Header selected");
252         } else if (
253             this.getRequestFromEntries().contains(InjectionModel.STAR)
254             && methodInjection != this.injectionModel.getMediatorMethod().getRequest()
255             && !isCheckingAllParam
256         ) {
257             throw new InjectionFailureException("param in Request selected but method URL or Header selected");
258         } else if (
259             this.getHeaderFromEntries().contains(InjectionModel.STAR)
260             && methodInjection != this.injectionModel.getMediatorMethod().getHeader()
261             && !isCheckingAllParam
262             && this.getCountHeadersWithStar() > 0
263         ) {
264             throw new InjectionFailureException("param in Header selected but method URL or Request selected");
265         }
266     }
267     
268     public void checkMethodNotEmpty() throws InjectionFailureException {
269         AbstractMethodInjection methodInjection = this.injectionModel.getMediatorUtils().connectionUtil().getMethodInjection();
270 
271         if (
272             methodInjection == this.injectionModel.getMediatorMethod().getQuery()
273             && this.getListQueryString().isEmpty()
274             && !this.injectionModel.getMediatorUtils().connectionUtil().getUrlBase().contains(InjectionModel.STAR)
275         ) {
276             throw new InjectionFailureException("empty URL param");
277         } else if (
278             methodInjection == this.injectionModel.getMediatorMethod().getRequest()
279             && this.getListRequest().isEmpty()
280         ) {
281             throw new InjectionFailureException("empty Request param");
282         } else if (
283             methodInjection == this.injectionModel.getMediatorMethod().getHeader()
284             && this.getListHeader().isEmpty()
285         ) {
286             throw new InjectionFailureException("empty Header param");
287         }
288     }
289     
290     public String initStar(SimpleEntry<String, String> parameterToInject) {
291         String characterInsertionByUser;
292         if (parameterToInject.getValue().contains(InjectionModel.STAR)) {  // star already set into value when checking all cookies
293             characterInsertionByUser = parameterToInject.getValue().replace(
294                 InjectionModel.STAR,
295                 InjectionModel.STAR
296                 + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
297             );
298         } else {
299             characterInsertionByUser = parameterToInject.getValue()
300             + (parameterToInject.getValue().matches("\\d$") ? "+" : StringUtils.EMPTY)  // required when char insertion numeric by user and not found, space not working as zipped
301             + InjectionModel.STAR
302             + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment();
303         }
304         // char insertion contains remainder payload when found
305         parameterToInject.setValue(
306             InjectionModel.STAR
307             + this.injectionModel.getMediatorEngine().getEngine().instance().endingComment()
308         );
309         return characterInsertionByUser;
310     }
311 
312     public void initQueryString(String urlQuery) throws MalformedURLException, URISyntaxException {
313         this.initQueryString(urlQuery, StringUtils.EMPTY);
314     }
315 
316     public void initQueryString(String urlQuery, String selectionCommand) throws MalformedURLException, URISyntaxException {
317         // Format and get rid of anchor fragment using native URL
318         var url = new URI(urlQuery).toURL();
319         
320         if (
321             StringUtils.isEmpty(urlQuery)
322             || StringUtils.isEmpty(url.getHost())
323         ) {
324             throw new MalformedURLException("empty URL");
325         }
326         
327         this.injectionModel.getMediatorUtils().connectionUtil().setUrlByUser(urlQuery);
328         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(urlQuery);
329         this.listQueryString.clear();
330         
331         // Parse url and GET query string
332         var regexQueryString = Pattern.compile("(.*\\?)(.*)").matcher(urlQuery);
333         if (!regexQueryString.find()) {
334             return;
335         }
336         
337         this.injectionModel.getMediatorUtils().connectionUtil().setUrlBase(regexQueryString.group(1));
338         
339         if (StringUtils.isNotEmpty(url.getQuery())) {
340             this.listQueryString = Pattern.compile("&")
341                 .splitAsStream(url.getQuery())
342                 .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
343                 .map(keyValue -> {
344                     var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_QUERY, StringUtils.EMPTY);
345                     return new SimpleEntry<>(
346                         keyValue[0],
347                         (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1])
348                         + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
349                     );
350                 }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));  // Fix #96224: ConcurrentModificationException
351         }
352     }
353 
354     public void initRequest(String rawRequest) {
355         this.initRequest(rawRequest, StringUtils.EMPTY);
356     }
357 
358     public void initRequest(String rawRequest, String selectionCommand) {
359         this.rawRequest = rawRequest;
360         this.listRequest.clear();
361         if (StringUtils.isNotEmpty(rawRequest)) {
362             if (this.isMultipartRequest || this.isRequestSoap()) {
363                 // Pass request containing star * param without any parsing
364                 this.listRequest = new CopyOnWriteArrayList<>(List.of(new SimpleEntry<>(
365                     rawRequest,
366                     StringUtils.EMPTY
367                 )));
368             } else {
369                 this.listRequest = Pattern.compile("&")
370                     .splitAsStream(rawRequest)
371                     .map(keyValue -> Arrays.copyOf(keyValue.split("="), 2))
372                     .map(keyValue -> {
373                         var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_REQUEST, StringUtils.EMPTY);
374                         return new SimpleEntry<>(
375                             keyValue[0],
376                             (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1].replace("\\n", "\n"))
377                             + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
378                         );
379                     }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
380             }
381         }
382     }
383 
384     public void initHeader(String rawHeader) {
385         this.initHeader(rawHeader, StringUtils.EMPTY);
386     }
387 
388     public void initHeader(String rawHeader, String selectionCommand) {
389         this.rawHeader = rawHeader;
390         this.listHeader.clear();
391         if (StringUtils.isNotEmpty(rawHeader)) {
392             this.listHeader = Pattern.compile("\\\\r\\\\n")
393                 .splitAsStream(rawHeader)
394                 .map(keyValue -> Arrays.copyOf(keyValue.split(":"), 2))
395                 .map(keyValue -> {
396                     var paramToAddStar = selectionCommand.replaceAll("^"+ ParameterUtil.PREFIX_COMMAND_HEADER, StringUtils.EMPTY);
397                     return new SimpleEntry<>(
398                         keyValue[0],
399                         (keyValue[1] == null ? StringUtils.EMPTY : keyValue[1])
400                         + (paramToAddStar.equals(keyValue[0]) ? InjectionModel.STAR : StringUtils.EMPTY)
401                     );
402                 }).collect(Collectors.toCollection(CopyOnWriteArrayList::new));
403         }
404     }
405     
406     public String getQueryStringFromEntries() {
407         return this.listQueryString.stream()
408             .filter(Objects::nonNull)
409             .map(entry -> String.format(
410                 ParameterUtil.FORMAT_KEY_VALUE,
411                 entry.getKey(),
412                 entry.getValue())
413             )
414             .collect(Collectors.joining("&"));
415     }
416 
417     public String getRequestFromEntries() {
418         return this.listRequest.stream()
419             .filter(Objects::nonNull)
420             .map(entry -> String.format(
421                 ParameterUtil.FORMAT_KEY_VALUE,
422                 entry.getKey(),
423                 StringUtils.isEmpty(entry.getValue()) ? StringUtils.EMPTY : entry.getValue()
424             ))
425             .collect(Collectors.joining("&"));
426     }
427     
428     public String getHeaderFromEntries() {
429         return this.listHeader.stream()
430             .filter(Objects::nonNull)
431             .map(entry -> String.format("%s:%s", entry.getKey(), entry.getValue()))
432             .collect(Collectors.joining("\\r\\n"));
433     }
434 
435     public boolean isRequestSoap() {
436         return this.rawRequest.trim().matches("(?s)^\\s*(<soapenv:|<\\?xml).*");
437     }
438 
439     
440     // Getters / setters
441     
442     public String getRawRequest() {
443         return this.rawRequest;
444     }
445 
446     public String getRawHeader() {
447         return this.rawHeader;
448     }
449 
450     public List<SimpleEntry<String, String>> getListRequest() {
451         return this.listRequest;
452     }
453 
454     public List<SimpleEntry<String, String>> getListHeader() {
455         return this.listHeader;
456     }
457 
458     public List<SimpleEntry<String, String>> getListQueryString() {
459         return this.listQueryString;
460     }
461     
462     public boolean isMultipartRequest() {
463         return this.isMultipartRequest;
464     }
465 }