Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Java: QL Query to Detect Security Sensitive non-CSPRNG usage #2694

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

JLLeitschuh
Copy link
Contributor

@JLLeitschuh JLLeitschuh commented Jan 24, 2020

GitHub Security Lab BB Submission

The goal of this query is to detect the use of a PRNG like java.util.Random, org.apache.commons.lang.RandomStringUtils, org.apache.commons.text.RandomStringGenerator, or java.util.concurrent.ThreadLocalRandom in a security sensitive context.

This vulnerability can have up to a CVSSv3 score of 9.8/10 depending upon the use of the insecure data generated.

Design

The design of the query is to find strings that are generated with some sort of insecure randomess component. Then use taint tracking to see if that string ever taints a value that has some sort of security sensitive indicator.

Currently the indicators I'm using are variables/methods that (when lowercase) match a name with the following predicate.

/**
 * A 'Sensitive Name' is something that indicates that the name has some sort of security
 * implication associated with it's usage.
 */
bindingset[name]
private predicate isSensitiveName(string name) {
  name.matches("%pass%") // also matches password
  or
  name.matches("%tok%") // also matches token
  or
  name.matches("%secret%")
  or
  name.matches("%reset%key%") and not name.matches("%value%") // Reduce false positive from 'keyValue' type of methods from maps
  or
  name.matches("%cred%") // also matches credentials
  or
  name.matches("%auth%") // also matches authentication
  or
  name.matches("%sess%id%") // also matches sessionid
}

I've debated adding %accnt%, %account%, and %trusted% as well.

Query Current Status

I'm currently not totally happy with the false-positive rate that this query produces and I'm looking for support/suggestions on how to increase its accuracy.

Additionally, because my 'sink' locations are inherently fuzzy you end up with multiple sink locations for the same source.

Determining whether or not a Random instance is actually a SecureRandom type at runtime is also inherently difficult. Currently, I'm only considering a Random variable 'safe' if at any point it's assigned as a final variable.

For example, these are 'safe' random values:

class Tester {
    final Random random = new SecureRandom();
    final Random random2;

    Tester() {
        this.random2 = new SecureRandom();
    }
}

If there's a better way to guess the type of a Random expression, I'd love to use it.

Known False Positives

Eclipse/Jetty

The way that Jetty sets it's random number generator is inherently 'safe', in probably 99% of cases.

https://github.com/eclipse/jetty.project/blob/69808d3851de31ffffb343115030d7bcf826ed01/jetty-server/src/main/java/org/eclipse/jetty/server/session/DefaultSessionIdManager.java#L367-L384

Query Results: https://lgtm.com/query/1859599740827953942/

Apache/shiro

https://github.com/apache/shiro/blob/cb3a1b3c0dcd8b89cc8d51ee9ede086882921faa/core/src/main/java/org/apache/shiro/session/mgt/eis/RandomSessionIdGenerator.java

Random random type is ambiguous in my query because there's a setRandom method that could be called by the user.

Apache/cloudstack

https://github.com/apache/cloudstack/blob/ac202639a5108d126ecea585c7e793696679dbe4/utils/src/main/java/com/cloud/utils/PasswordGenerator.java#L53-L107

Can't track that the generateLowercaseChar will always be passed a SecureRandom instance.

Elasticsearch/Elasticsearch

https://github.com/elastic/elasticsearch/blob/a5c40b3d828fe6cc5d722ec134305864d31c6515/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java

Can't track that the Random instance passed to getUUIDBytes will always be an instance of SecureRandom.

More... TODO

Test Repository

I've been testing this query against the code I've been uploading to this repository. Ideally, this repository would get contributed as a part of the tests for this PR when it's complete.

See: https://github.com/JLLeitschuh/bad-random-examples

@JLLeitschuh JLLeitschuh requested review from felicitymay and a team as code owners January 24, 2020 21:24
Copy link
Contributor Author

@JLLeitschuh JLLeitschuh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just some initial thoughts and self-review

Comment on lines 1 to 154
)
}

private class PredictableJavaStdlibRandomMethodAcccess extends PredictableRandomMethodAccess {
PredictableJavaStdlibRandomMethodAcccess() {
this.getReceiverType() instanceof JavaStdlibInsecureRandomType and
not isSecureRuntimeVarAccess(this.getQualifier())
}
}

/**
* A byte array tainted by insecure RNG.
*
* Examples:
*
* ```
* byte[] array = new byte[];
* new Random().nextBytes(array);
* ```
*/
private class TaintedByteArrayWrite extends VarAccess {
TaintedByteArrayWrite() {
exists(PredictableJavaStdlibRandomMethodAcccess insecureMethodAccess, MethodAccess ma |
ma.getMethod().hasName("nextBytes") and
ma = insecureMethodAccess and
ma.getArgument(0) = this
)
}
}

class SecureRandomMethodAccess extends MethodAccess {
SecureRandomMethodAccess() { this.getReceiverType() instanceof TypeSecureRandom }
}

/**
* Intermediate tracking step to find instances of RandomStringGenerator that are "safely" created.
* A "safely" created RandomStringGenerator is defined as having used a SecureRandom method in the
* RandomStringGenerator.Builder::usingRandom lambda.
*/
class SafeRandomStringGeneratorFlowConfig extends TaintTracking2::Configuration {
SafeRandomStringGeneratorFlowConfig() { this = "RNG:SafeRandomStringGeneratorFlowConfig" }

private predicate isUsingSafeUsingRandomSupplier(MethodAccess source) {
exists(
ApacheRandomStringGeneratorBuilderUsingRandomMethod usingRandomMethod,
FunctionalExpr functionalExpr, SecureRandomMethodAccess secureRandomMethodAccess
|
usingRandomMethod = source.getMethod() and
source.getArgument(0).getProperExpr() = functionalExpr and
// Some SecureRandom method is used in the scope of the functional expression
functionalExpr.asMethod() = secureRandomMethodAccess.getEnclosingCallable()
)
}

override predicate isSource(DataFlow::Node src) { isUsingSafeUsingRandomSupplier(src.asExpr()) }

/**
* A sink is any RandomStringGenerator::generate method.
*/
override predicate isSink(DataFlow::Node sink) {
exists(ApacheRandomStringGeneratorType generatorType |
sink.asExpr().(MethodAccess).getMethod() = generatorType.getGenerateMethod()
)
}

private predicate isBuilderStep(MethodAccess previous, MethodAccess next) {
exists(ApacheRandomStringGeneratorBuilderType builderType |
builderType.getAMethod() = previous.getMethod() and
previous = next.getQualifier()
)
}

private predicate isGeneratorCallStep(Expr previous, MethodAccess next) {
exists(ApacheRandomStringGeneratorType generatorType |
generatorType.getGenerateMethod() = next.getMethod() and
previous = next.getQualifier()
)
}

override predicate isAdditionalTaintStep(DataFlow::Node previous, DataFlow::Node next) {
isBuilderStep(previous.asExpr(), next.asExpr()) or
isGeneratorCallStep(previous.asExpr(), next.asExpr())
}
}

private class TaintedRandomStringGeneratorMethodAccess extends PredictableRandomMethodAccess {
TaintedRandomStringGeneratorMethodAccess() {
this.getMethod().getDeclaringType() instanceof ApacheRandomStringGeneratorType and
not exists(SafeRandomStringGeneratorFlowConfig safeFlowSource |
safeFlowSource.hasFlowTo(DataFlow::exprNode(this))
)
}
}

private class PredictableRandomTaintedMethodAccessSource extends PredictableRandomFlowSource {
PredictableRandomTaintedMethodAccessSource() {
this.asExpr() instanceof PredictableRandomMethodAccess or
this.asExpr() instanceof TaintedByteArrayWrite
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should probably live somewhere else (and maybe be broken up), but leaving it here for now allows me to continue to iterate on the query against LGTM.com

Comment on lines 1 to 52
import java

class UUIDCreationExp extends ClassInstanceExpr {
UUIDCreationExp() { this.getConstructedType() instanceof TypeUUID }
}

class ApacheRandomStringUtilsType extends RefType {
ApacheRandomStringUtilsType() {
this.hasQualifiedName("org.apache.commons.lang", "RandomStringUtils") or
this.hasQualifiedName("org.apache.commons.lang3", "RandomStringUtils")
}
}

class ApacheRandomStringGeneratorType extends RefType {
ApacheRandomStringGeneratorType() {
this.hasQualifiedName("org.apache.commons.text", "RandomStringGenerator")
}

Method getGenerateMethod() {
exists(Method m |
m.getDeclaringType() = this and
m.hasName("generate") and
m = result
)
}
}

class ApacheRandomStringGeneratorBuilderType extends RefType {
ApacheRandomStringGeneratorBuilderType() {
this.hasQualifiedName("org.apache.commons.text", "RandomStringGenerator$Builder")
}
}

class ApacheRandomStringGeneratorBuilderUsingRandomMethod extends Method {
ApacheRandomStringGeneratorBuilderUsingRandomMethod() {
this.getDeclaringType() instanceof ApacheRandomStringGeneratorBuilderType and
this.hasName("usingRandom")
}
}

class ApacheTextRandomProviderType extends RefType {
ApacheTextRandomProviderType() {
this.hasQualifiedName("org.apache.commons.text", "TextRandomProvider")
}
}

class JavaStdlibInsecureRandomType extends RefType {
JavaStdlibInsecureRandomType() {
this.hasQualifiedName("java.util.concurrent", "ThreadLocalRandom") or
this.hasQualifiedName("java.util", "Random")
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another file that should get moved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add java.util.SplittableRandom?

Comment on lines +65 to +83
/**
* A byte array tainted by insecure RNG.
*
* Examples:
*
* ```
* byte[] array = new byte[];
* new Random().nextBytes(array);
* ```
*/
private class TaintedByteArrayWrite extends VarAccess {
TaintedByteArrayWrite() {
exists(PredictableJavaStdlibRandomMethodAcccess insecureMethodAccess, MethodAccess ma |
ma.getMethod().hasName("nextBytes") and
ma = insecureMethodAccess and
ma.getArgument(0) = this
)
}
}
Copy link
Contributor Author

@JLLeitschuh JLLeitschuh Jan 24, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just discovered that this has a false positive on:

Random random = new SecureRandom();
byte[] array = new byte[];
random.nextBytes(array);

@aschackmull
Copy link
Contributor

If there's a better way to guess the type of a Random expression, I'd love to use it.

We have the TypeFlow library, which can do a bit in terms of giving improved type bounds for expressions and fields. It's currently mostly used as part of the virtual dispatch resolution, but it should also be a fine stand-alone library. Just import semmle.code.java.dataflow.TypeFlow and try out the predicate exprTypeFlow(Expr e, RefType t, boolean exact) and predicate fieldTypeFlow(Field f, RefType t, boolean exact) predicates. Note that those predicates only have results for expressions and fields for which the library has determined a type bound that's likely to be better than the static type.

@yo-h
Copy link
Contributor

yo-h commented Jan 28, 2020

There are a couple of similar "insecure randomness" queries for other languages. Prior to merging, we should consider whether to give this query the same ID (java/insecure-randomness), or whether there is a good reason to differentiate this query from those in the other languages.

@JLLeitschuh
Copy link
Contributor Author

@aschackmull The exprTypeFlow was a good tip, thanks.

Got anything for this?

private static final SecureRandom RANDOM_INSTANCE = new SecureRandom();
private String generateString(Random random) {
 byte[] array = new byte[];
 random.nextBytes(array);
 return new String(array);
}

public String generateSecure() { // Thinks this method is tainted
  return generateString(RANDOM_INSTANCE);
}

I'm running into this case still with Elasticsearch:
https://github.com/elastic/elasticsearch/blob/a5c40b3d828fe6cc5d722ec134305864d31c6515/server/src/main/java/org/elasticsearch/common/RandomBasedUUIDGenerator.java

@aschackmull
Copy link
Contributor

Your example code snippet should work with exprTypeFlow. However, a prerequisite for inferring a type bound for the parameter is that generateString is private - otherwise it might be called from someplace else with something that isn't a SecureRandom.
Abstracting a bit, the typeflow library is designed for universal flow, i.e. merge points in the flow graph are treated with universal quantification, whereas the general dataflow library essentially uses existential quantification.
If you're generating too many FPs, it might be worth it to consider whether a result should be ruled out by the existence of a SecureRandom that reaches the relevant node, instead of trying to universally prove that all Randoms that get to that point are in fact SecureRandom. This would be slightly more heuristic, but might work better if you're seeing too many FPs.

@aschackmull
Copy link
Contributor

In the ElasticSearch example, the fact that getBase64UUID is public means that we cannot give a tighter bound on the type of the parameter in getUUIDBytes.

@aschackmull
Copy link
Contributor

If the relevant FP in ElasticSearch is only in getBase64UUIDSecureString via a data flow return from getUUIDBytes then it is theoretically possible to combine the type flow analysis with the subgraph of data flow related to the source-sink pair and get a better type bound that way. I have plans to do this in order to improve virtual dispatch in data flow, but for this use case it would probably need to be exposed in a slightly different way. These sorts of extensions to data flow are, however, quite non-trivial.

@aschackmull
Copy link
Contributor

An alternative approach is to perhaps make method calls like nextBytes into additional steps instead of sources, and trace the source further back to a constructor call that definitely isn't new SecureRandom, but rather e.g. new Random. That would deal with the ElasticSearch FP, I think.

ApacheRandomStringUtilsType() {
this.hasQualifiedName("org.apache.commons.lang", "RandomStringUtils") or
this.hasQualifiedName("org.apache.commons.lang3", "RandomStringUtils")
}
Copy link
Contributor

@pwntester pwntester Feb 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you tried non-string variants (eg: org.apache.commons.lang3.RandomUtils)? do they generate a high rate of FPs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on what you mean?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are accounting for org.apache.commons.lang3.RandomStringUtils, but not for org.apache.commons.lang3.RandomUtils. Did you miss that class or did you discard it because it was generating too many false positives?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, nope, I just didn't know it existed, thanks!

@JLLeitschuh
Copy link
Contributor Author

This is still a WIP, I'll be coming back to it soon.

@JLLeitschuh JLLeitschuh requested a review from a team as a code owner June 16, 2020 01:37
…neration

* master: (2719 commits)
  C++: Fix QLDoc on `FormattingFunction` library
  Update docs/language/ql-handbook/name-resolution.rst
  Mention `libraryPathDependencies`
  JS: Update another test
  JS: Autoformat
  JS: Update test
  QL reference: Update process for name resolution
  JS: Propagate locally returned functions out of calls
  JS: Add test
  Add link to Java regex Pattern documentation to language.rst
  Fix typo
  C++: Fix reference to `Block`
  JS: Fix a misleading javadoc comment
  CodeQL for Go: Edit AST reference
  C#: Rename a class
  C#: Recognize more calls to `IHtmlHelper.Raw`
  Give general syntax instead of examples for exprs
  Add more links to java AST class reference
  Move explicit hyperlink targets to the bottom
  Add references to the AST class reference for go
  ...
@xcorail
Copy link

xcorail commented Jul 8, 2020

This is still a WIP, I'll be coming back to it soon.

Hey @JLLeitschuh do you intend to come back to this PR?

@JLLeitschuh
Copy link
Contributor Author

Hey @JLLeitschuh do you intend to come back to this PR?

Very much so. It's been on the back-burner of my mind for the past few months. I probably won't have time/energy to finish it until after August.

Copy link
Contributor

@felicitymay felicitymay left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, I'm waiting to review the text until this PR is nearly ready to be merged.

@JLLeitschuh
Copy link
Contributor Author

I will get back to this eventually

@JLLeitschuh
Copy link
Contributor Author

JLLeitschuh commented Feb 6, 2024

@egregius313 rather old query, but you may find some things valuable, and worth including in the current variant of this query that ended up being merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants