Skip to content

Commit

Permalink
SNOW-62511: Mask AWS password in a query
Browse files Browse the repository at this point in the history
  • Loading branch information
smtakeda authored and ankit-bhatnagar167 committed Feb 22, 2019
1 parent 923af4b commit f197b85
Show file tree
Hide file tree
Showing 3 changed files with 235 additions and 2 deletions.
11 changes: 9 additions & 2 deletions src/main/java/net/snowflake/client/core/SFStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import net.snowflake.client.jdbc.telemetry.TelemetryUtil;
import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;
import net.snowflake.client.util.SecretDetector;
import net.snowflake.common.core.SqlState;
import org.apache.http.client.methods.HttpRequestBase;

Expand Down Expand Up @@ -220,7 +221,10 @@ SFBaseResultSet executeQueryInternal(
{
resetState();

logger.debug( "executeQuery: {}", sql);
if (logger.isDebugEnabled())
{
logger.debug( "executeQuery: {}", SecretDetector.maskAWSSecret(sql));
}

if (session.isClosed())
{
Expand Down Expand Up @@ -649,7 +653,10 @@ public SFBaseResultSet execute(String sql,

session.injectedDelay();

logger.debug("execute: {}", sql);
if (logger.isDebugEnabled())
{
logger.debug("execute: {}", SecretDetector.maskAWSSecret(sql));
}

String trimmedSql = sql.trim();

Expand Down
181 changes: 181 additions & 0 deletions src/main/java/net/snowflake/client/util/SecretDetector.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
/*
* Copyright (c) 2012-2018 Snowflake Computing Inc. All rights reserved.
*/

package net.snowflake.client.util;

import net.snowflake.client.log.SFLogger;
import net.snowflake.client.log.SFLoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Search for credentials in a sql text
*
*/
public class SecretDetector
{
// We look for long base64 encoded (and perhaps also URL encoded - hence the
// '%' character in the regex) strings.
// This will match some things that it shouldn't. The minimum number of
// characters is essentially a random choice - long enough to not mask other
// strings but not so long that it might miss things.
private static final Pattern GENERIC_CREDS_PATTERN = Pattern.compile(
"([a-z0-9+/%]{18,})", Pattern.CASE_INSENSITIVE);

private static final Pattern AWS_KEY_PATTERN = Pattern.compile(
"(aws_key_id)|(aws_secret_key)|(access_key_id)|(secret_access_key)",
Pattern.CASE_INSENSITIVE);

// Used for detecting tokens in serialized JSON
private static final Pattern AWS_TOKEN_PATTERN = Pattern.compile(
"(accessToken|tempToken|keySecret)\"\\s*:\\s*\"([a-z0-9/+]{32,}={0,2})\"",
Pattern.CASE_INSENSITIVE);
private static final Pattern SAS_TOKEN_PATTERN = Pattern.compile(
"sig=([a-z0-9%]{32,})", Pattern.CASE_INSENSITIVE);

private static final int LOOK_AHEAD = 10;

// only attempt to find secrets in its leading 100Kb SNOW-30961
private static final int MAX_LENGTH = 100 * 1000;

private static final SFLogger LOGGER = SFLoggerFactory.getLogger(
SecretDetector.class);

/**
* Find all the positions of aws key id and aws secret key.
* The time complexity is O(n)
*
* @param text the sql text which may contain aws key
* @return Return a list of begin/end positions of aws key id and
* aws secret key.
*/
private static List<SecretRange> getAWSSecretPos(String text)
{
// log before and after in case this is causing StackOverflowError
LOGGER.debug("pre-regex getAWSSecretPos");

Matcher matcher = AWS_KEY_PATTERN.matcher(text);

ArrayList<SecretRange> awsSecretRanges = new ArrayList<>();

while (matcher.find())
{
int beginPos = Math.min(matcher.end() + LOOK_AHEAD, text.length());

while (beginPos > 0 && beginPos < text.length() &&
isBase64(text.charAt(beginPos)))
{
beginPos--;
}

int endPos = Math.min(matcher.end() + LOOK_AHEAD, text.length());

while (endPos < text.length() && isBase64(text.charAt(endPos)))
{
endPos++;
}

if (beginPos < text.length() && endPos <= text.length()
&& beginPos >= 0 && endPos >= 0)
{
awsSecretRanges.add(new SecretRange(beginPos + 1, endPos));
}
}

LOGGER.debug("post-regex getAWSSecretPos");

return awsSecretRanges;
}

/**
* Find all the positions of long base64 encoded strings that are typically
* indicative of secrets or other keys.
* The time complexity is O(n)
*
* @param text the sql text which may contain secrets
* @return Return a list of begin/end positions of keys in the string
*/
private static List<SecretRange> getGenericSecretPos(String text)
{
// log before and after in case this is causing StackOverflowError
LOGGER.debug("pre-regex getGenericSecretPos");

Matcher matcher = GENERIC_CREDS_PATTERN.matcher(
text.length() <= MAX_LENGTH ? text : text.substring(0, MAX_LENGTH));

ArrayList<SecretRange> awsSecretRanges = new ArrayList<>();

while (matcher.find())
{
awsSecretRanges.add(new SecretRange(matcher.start(), matcher.end()));
}

LOGGER.debug("post-regex getGenericSecretPos");

return awsSecretRanges;
}

private static boolean isBase64(char ch)
{
return ('A' <= ch && ch <= 'Z')
|| ('a' <= ch && ch <= 'z')
|| ('0' <= ch && ch <= '9')
|| ch == '+'
|| ch == '/';
}

/**
* mask AWS secret in the input string
* @param sql
* @return masked string
*/
public static String maskAWSSecret(String sql)
{
List<SecretDetector.SecretRange> secretRanges =
SecretDetector.getAWSSecretPos(sql);
for (SecretDetector.SecretRange secretRange : secretRanges)
{
sql = maskText(sql, secretRange.beginPos, secretRange.endPos);
}
return sql;
}

/**
* Masks given text between begin position and end position.
*
* @param text text to mask
* @param begPos begin position (inclusive)
* @param endPos end position (exclusive)
* @return masked text
*/
private static String maskText(String text, int begPos, int endPos)
{
// Convert the SQL statement to a char array to obe able to modify it.
char[] chars = text.toCharArray();

// Mask the value in the SQL statement using *.
for (int curPos = begPos; curPos < endPos; curPos++)
{
chars[curPos] = '☺';
}

// Convert it back to a string
return String.valueOf(chars);
}

static class SecretRange
{
final int beginPos;
final int endPos;

SecretRange(int beginPos, int endPos)
{
this.beginPos = beginPos;
this.endPos = endPos;
}
}
}
45 changes: 45 additions & 0 deletions src/test/java/net/snowflake/client/util/SecretDetectorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package net.snowflake.client.util;

import org.junit.Test;

import static org.hamcrest.MatcherAssert.assertThat;

public class SecretDetectorTest
{
@Test
public void testMaskAWSSecret()
{
String sql = "copy into 's3://xxxx/test' from \n" +
"(select seq1(), random()\n" +
", random(), random(), random(), random()\n" +
", random(), random(), random(), random()\n" +
", random() , random(), random(), random()\n" +
"\tfrom table(generator(rowcount => 10000)))\n" +
"credentials=(\n" +
" aws_key_id='xxdsdfsafds'\n" +
" aws_secret_key='safas+asfsad+safasf'\n" +
" )\n" +
"OVERWRITE = TRUE \n" +
"MAX_FILE_SIZE = 500000000 \n" +
"HEADER = TRUE \n" +
"FILE_FORMAT = (TYPE = PARQUET SNAPPY_COMPRESSION = TRUE )\n" +
";";
String correct = "copy into 's3://xxxx/test' from \n" +
"(select seq1(), random()\n" +
", random(), random(), random(), random()\n" +
", random(), random(), random(), random()\n" +
", random() , random(), random(), random()\n" +
"\tfrom table(generator(rowcount => 10000)))\n" +
"credentials=(\n" +
" aws_key_id='☺☺☺☺☺☺☺☺☺☺☺'\n" +
" aws_secret_key='☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺☺'\n" +
" )\n" +
"OVERWRITE = TRUE \n" +
"MAX_FILE_SIZE = 500000000 \n" +
"HEADER = TRUE \n" +
"FILE_FORMAT = (TYPE = PARQUET SNAPPY_COMPRESSION = TRUE )\n" +
";";
String masked = SecretDetector.maskAWSSecret(sql);
assertThat("secret masked", correct.compareTo(masked)==0);
}
}

0 comments on commit f197b85

Please sign in to comment.