Skip to content
This repository has been archived by the owner on Jul 22, 2021. It is now read-only.

Commit

Permalink
NIFIREG-415 Add Support for Unicode in X-ProxiedEntitiesChain
Browse files Browse the repository at this point in the history
- Adds detection and encoding of non-ascii characters to creation of chain
- Adds unit tests and integration tests that use proxied entities with Unicode
- Refactors ProxiedEntityRequestConfig to compute header value in constructor
  • Loading branch information
kevdoran committed Sep 14, 2020
1 parent 3739334 commit 0afa3a1
Show file tree
Hide file tree
Showing 15 changed files with 994 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,32 @@
*/
package org.apache.nifi.registry.client.impl.request;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.nifi.registry.client.RequestConfig;
import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
* Implementation of RequestConfig that produces headers for a request with proxied-entities.
*/
public class ProxiedEntityRequestConfig implements RequestConfig {

private final String[] proxiedEntities;
private final String proxiedEntitiesChain;

public ProxiedEntityRequestConfig(final String... proxiedEntities) {
this.proxiedEntities = Validate.notNull(proxiedEntities);
Validate.notNull(proxiedEntities);
this.proxiedEntitiesChain = ProxiedEntitiesUtils.getProxiedEntitiesChain(proxiedEntities);
}

@Override
public Map<String, String> getHeaders() {
final String proxiedEntitiesValue = getProxiedEntitesValue(proxiedEntities);

final Map<String,String> headers = new HashMap<>();
if (proxiedEntitiesValue != null) {
headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesValue);
if (proxiedEntitiesChain != null) {
headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, proxiedEntitiesChain);
}
return headers;
}

private String getProxiedEntitesValue(final String[] proxiedEntities) {
if (proxiedEntities == null) {
return null;
}

final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
.map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
return StringUtils.join(proxiedEntityChain, "");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -39,6 +41,55 @@ public class ProxiedEntitiesUtils {

private static final String ANONYMOUS_CHAIN = "<>";

/**
* Formats a list of DN/usernames to be set as a HTTP header using well known conventions.
*
* @param proxiedEntities the raw identities (usernames and DNs) to be formatted as a chain
* @return the value to use in the X-ProxiedEntitiesChain header
*/
public static String getProxiedEntitiesChain(final String[] proxiedEntities) {
if (proxiedEntities == null) {
return null;
}

final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
.map(ProxiedEntitiesUtils::formatProxyDn)
.collect(Collectors.toList());
return StringUtils.join(proxiedEntityChain, "");
}

/**
* Tokenizes the specified proxy chain.
*
* @param rawProxyChain raw chain
* @return tokenized proxy chain
*/
public static List<String> tokenizeProxiedEntitiesChain(final String rawProxyChain) {
final List<String> proxyChain = new ArrayList<>();
if (!StringUtils.isEmpty(rawProxyChain)) {
// Split the String on the >< token
rawProxyChain.split("><");
List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));

// Remove the leading < from the first element
elements.set(0, elements.get(0).replaceFirst(LT, ""));

// Remove the trailing > from the last element
final int last = elements.size() - 1;
final String lastElement = elements.get(last);
if (lastElement.endsWith(GT)) {
elements.set(last, lastElement.substring(0, lastElement.length() - 1));
}

// Unsanitize each DN and collect back
elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());

proxyChain.addAll(elements);
}

return proxyChain;
}

/**
* Formats the specified DN to be set as a HTTP header using well known conventions.
*
Expand All @@ -50,11 +101,20 @@ public static String formatProxyDn(String dn) {
}

/**
* If a user provides a DN with the sequence '><', they could escape the tokenization process and impersonate another user.
* Sanitizes a DN for safe and lossless transmission.
*
* Sanitization requires:
* <ol>
* <li>Encoded so that it can be sent losslessly using US-ASCII (the character set of HTTP Header values)</li>
* <li>Resilient to a DN with the sequence '><' to attempt to escape the tokenization process and impersonate another user.</li>
* </ol>
*
* <p>
* Example:
* <p>
* Provided DN: {@code jdoe><alopresto} -> {@code <jdoe><alopresto><proxy...>} would allow the user to impersonate jdoe
* <p>Алйс
* Provided DN: {@code Алйс} -> {@code <Алйс>} cannot be encoded/decoded as ASCII
*
* @param rawDn the unsanitized DN
* @return the sanitized DN
Expand All @@ -63,11 +123,32 @@ private static String sanitizeDn(String rawDn) {
if (StringUtils.isEmpty(rawDn)) {
return rawDn;
} else {
String sanitizedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
if (!sanitizedDn.equals(rawDn)) {
logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + sanitizedDn + "]");

// First, escape any GT [>] or LT [<] characters, which are not safe
final String escapedDn = rawDn.replaceAll(GT, ESCAPED_GT).replaceAll(LT, ESCAPED_LT);
if (!escapedDn.equals(rawDn)) {
logger.warn("The provided DN [" + rawDn + "] contained dangerous characters that were escaped to [" + escapedDn + "]");
}

// Second, check for characters outside US-ASCII.
// This is necessary because X509 Certs can contain international/Unicode characters,
// but this value will be passed in an HTTP Header which must be US-ASCII.
// If non-ascii characters are present, base64 encode the DN and wrap in <angled-brackets>,
// to indicate to the receiving end that the value must be decoded.
// Note: We could have decided to always base64 encode these values,
// not only to avoid the isPureAscii(...) check, but also as a
// method of sanitizing GT [>] or LT [<] chars. However, there
// are advantages to encoding only when necessary, namely:
// 1. Backwards compatibility
// 2. Debugging this X-ProxiedEntitiesChain headers is easier unencoded.
// This algorithm can be revisited as part of the next major version change.
if (isPureAscii(escapedDn)) {
return escapedDn;
} else {
final String encodedDn = base64Encode(escapedDn);
logger.debug("The provided DN [" + rawDn + "] contained non-ASCII characters and was encoded as [" + encodedDn + "]");
return encodedDn;
}
return sanitizedDn;
}
}

Expand All @@ -77,51 +158,66 @@ private static String sanitizeDn(String rawDn) {
* Example:
* <p>
* {@code alopresto\>\<proxy1} -> {@code alopresto><proxy1}
* <p>
* {@code %D0%90%D0%BB%D0%B9%D1%81} -> {@code Алйс}
*
* @param sanitizedDn the sanitized DN
* @return the original DN
*/
private static String unsanitizeDn(String sanitizedDn) {
private static String unsanitizeDn(final String sanitizedDn) {
if (StringUtils.isEmpty(sanitizedDn)) {
return sanitizedDn;
} else {
String unsanitizedDn = sanitizedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
if (!unsanitizedDn.equals(sanitizedDn)) {
final String decodedDn;
if (isBase64Encoded(sanitizedDn)) {
decodedDn = base64Decode(sanitizedDn);
logger.debug("The provided DN [" + sanitizedDn + "] had been encoded, and was reconstituted to the original DN [" + decodedDn + "]");
} else {
decodedDn = sanitizedDn;
}
final String unsanitizedDn = decodedDn.replaceAll(ESCAPED_GT, GT).replaceAll(ESCAPED_LT, LT);
if (!unsanitizedDn.equals(decodedDn)) {
logger.warn("The provided DN [" + sanitizedDn + "] had been escaped, and was reconstituted to the dangerous DN [" + unsanitizedDn + "]");
}
return unsanitizedDn;
}
}

/**
* Tokenizes the specified proxy chain.
* Base64 encodes a DN and wraps it in angled brackets to indicate the value is base64 and not a raw DN.
*
* @param rawProxyChain raw chain
* @return tokenized proxy chain
* @param rawValue The value to encode
* @return A string containing a wrapped, encoded value.
*/
public static List<String> tokenizeProxiedEntitiesChain(String rawProxyChain) {
final List<String> proxyChain = new ArrayList<>();
if (!StringUtils.isEmpty(rawProxyChain)) {
// Split the String on the >< token
List<String> elements = Arrays.asList(StringUtils.splitByWholeSeparatorPreserveAllTokens(rawProxyChain, "><"));

// Unsanitize each DN and collect back
elements = elements.stream().map(ProxiedEntitiesUtils::unsanitizeDn).collect(Collectors.toList());

// Remove the leading < from the first element
elements.set(0, elements.get(0).replaceFirst(LT, ""));
private static String base64Encode(final String rawValue) {
final String base64String = Base64.getEncoder().encodeToString(rawValue.getBytes(StandardCharsets.UTF_8));
final String wrappedEncodedValue = LT + base64String + GT;
return wrappedEncodedValue;
}

// Remove the trailing > from the last element
int last = elements.size() - 1;
String lastElement = elements.get(last);
if (lastElement.endsWith(GT)) {
elements.set(last, lastElement.substring(0, lastElement.length() - 1));
}
/**
* Performs the reverse of ${@link #base64Encode(String)}.
*
* @param encodedValue the encoded value to decode.
* @return The original, decoded string.
*/
private static String base64Decode(final String encodedValue) {
final String base64String = encodedValue.substring(1, encodedValue.length() - 1);
return new String(Base64.getDecoder().decode(base64String), StandardCharsets.UTF_8);
}

proxyChain.addAll(elements);
}
/**
* Check if a value has been encoded by ${@link #base64Encode(String)}, and therefore needs to be decoded.
*
* @param token the value to check
* @return true if the value is encoded, false otherwise.
*/
private static boolean isBase64Encoded(final String token) {
return token.startsWith(LT) && token.endsWith(GT);
}

return proxyChain;
private static boolean isPureAscii(final String stringWithUnknownCharacters) {
return StandardCharsets.US_ASCII.newEncoder().canEncode(stringWithUnknownCharacters);
}

}
Loading

0 comments on commit 0afa3a1

Please sign in to comment.