View Javadoc

1   package com.atlassian.asap.core.validator;
2   
3   import com.atlassian.asap.core.exception.InvalidHeaderException;
4   import com.google.common.collect.ImmutableSet;
5   import org.apache.commons.lang3.StringUtils;
6   import org.apache.commons.lang3.builder.EqualsBuilder;
7   import org.apache.commons.lang3.builder.HashCodeBuilder;
8   import org.apache.commons.lang3.builder.ToStringBuilder;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  
12  import java.util.List;
13  import java.util.Objects;
14  import java.util.Set;
15  import java.util.regex.Pattern;
16  import java.util.stream.Collectors;
17  
18  import static org.apache.commons.lang3.StringUtils.isBlank;
19  
20  /**
21   * Represents a validated key id of the JWT Header. A validated key id is safe to be used in the path component of a url
22   * without causing path traversal.
23   */
24  public final class ValidatedKeyId {
25      /**
26       * Subset of the valid characters for URI Paths derived from <a href="https://tools.ietf.org/html/rfc3986#section-3.3">RFC3986</a>.
27       * The subset has been chosen to minimise security risks, including variable expansion,
28       * query parameters injection and unexpected string terminators.
29       */
30      public static final Pattern PATH_PATTERN = Pattern.compile("^[\\w.\\-\\+/]*$");
31  
32      private static final Set<String> PATH_TRAVERSAL_COMPONENTS = ImmutableSet.of(".", "..");
33      private static final Pattern PATH_SPLITTER = Pattern.compile("/");
34  
35      private static final Logger logger = LoggerFactory.getLogger(ValidatedKeyId.class);
36  
37      private final String keyId;
38  
39      private ValidatedKeyId(String validatedKeyId) {
40          this.keyId = Objects.requireNonNull(validatedKeyId);
41      }
42  
43      /**
44       * Validates the given key id and upon successful validation returns a {@link ValidatedKeyId} instance.
45       *
46       * @param unvalidatedKeyId a key identifier for public or private key
47       * @return a validated key id instance if the given key id passes the validation
48       * @throws InvalidHeaderException if the key id is invalid or unsafe to use
49       */
50      public static ValidatedKeyId validate(String unvalidatedKeyId) throws InvalidHeaderException {
51          if (isBlank(unvalidatedKeyId)) {
52              logger.debug("Rejecting absent or blank kid");
53              throw new InvalidHeaderException("The kid header is required");
54          }
55  
56          // Prevent directory traversal (e.g. ../../../etc/passwd)
57          List<String> pathComponents = PATH_SPLITTER.splitAsStream(unvalidatedKeyId)
58                  .map(StringUtils::trim)
59                  .collect(Collectors.toList());
60          if (pathComponents.stream().anyMatch(PATH_TRAVERSAL_COMPONENTS::contains)) {
61              logger.debug("Rejecting kid value {} because it contains path traversal", unvalidatedKeyId);
62              throw new InvalidHeaderException("Path traversal components not allowed in kid");
63          }
64  
65          // Prevent empty components (e.g. foo//bar/baz), also covers the case of leading slashes
66          if (pathComponents.stream().anyMatch(StringUtils::isBlank)) {
67              logger.debug("Rejecting kid value {} because it is in invalid format", unvalidatedKeyId);
68              throw new InvalidHeaderException("Invalid format of kid");
69          }
70  
71          // the kid must be a valid URL path fragment (e.g., no special chars, no '?')
72          if (!PATH_PATTERN.matcher(unvalidatedKeyId).matches()) {
73              logger.debug("Rejecting kid value {} because it contains invalid characters", unvalidatedKeyId);
74              throw new InvalidHeaderException("Invalid character found in kid");
75          }
76  
77          return new ValidatedKeyId(unvalidatedKeyId);
78      }
79  
80      /**
81       * @return a validated key id that is safe to be used in the path component of a url without causing path traversal
82       */
83      public String getKeyId() {
84          return keyId;
85      }
86  
87      @Override
88      public boolean equals(Object o) {
89          if (this == o) return true;
90  
91          if (o == null || getClass() != o.getClass()) return false;
92  
93          ValidatedKeyId that = (ValidatedKeyId) o;
94  
95          return new EqualsBuilder()
96                  .append(keyId, that.keyId)
97                  .isEquals();
98      }
99  
100     @Override
101     public int hashCode() {
102         return new HashCodeBuilder()
103                 .append(keyId)
104                 .toHashCode();
105     }
106 
107     @Override
108     public String toString() {
109         return new ToStringBuilder(this)
110                 .append("keyId", keyId)
111                 .toString();
112     }
113 }