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: Add Guard Classes for checking OS & unify System Property Access #8032

Merged
merged 28 commits into from
Mar 18, 2022

Conversation

JLLeitschuh
Copy link
Contributor

@JLLeitschuh JLLeitschuh commented Feb 14, 2022

This is useful when you have a bit of logic that is only vulnerable on specific OSes. Additionally, it unifies the access of all system properties (from all libraries and JDK APIs) into one method call getSystemProperty(_).

}
} else if (!Files.exists(tempDirChild)) {
// On Windows, we still need to create the directory, when it doesn't already exist.
Files.createDirectory(tempDirChild); // GOOD: Windows doesn't share the temp directory between users
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without the OS guard changes, this gets flagged as vulnerable

}
} else if (!Files.exists(tempDirChild)) {
// On Windows, we still need to create the directory, when it doesn't already exist.
Files.createDirectories(tempDirChild); // GOOD: Windows doesn't share the temp directory between users
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without the OS guard changes, this also gets flagged as vulnerable

@@ -30,7 +34,7 @@ void exampleSafeWithWindowsSupportFile() {
createTempFile(tempChildFile.toPath()); // GOOD: Good has permissions `-rw-------`
}

static void createTempFile(Path tempDir) {
static void createTempFile(Path tempDirChild) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Simple fix because this wouldn't have compiled earlier

@JLLeitschuh
Copy link
Contributor Author

Thanks @intrigus-lgtm for the feedback! 🙂

java/ql/lib/semmle/code/java/StringCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/StringCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/os/OSCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/os/OSCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/os/OSCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/os/OSCheck.qll Outdated Show resolved Hide resolved
Copy link
Contributor

@Marcono1234 Marcono1234 left a comment

Choose a reason for hiding this comment

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

Would it make sense to also consider OperatingSystemMXBean.getName()?

Sometimes the OS path separator (File.pathSeparator, File.pathSeparatorChar) and OS file path separator (File.separator, File.separatorChar) are used for OS detection; examples:

Would it make sense to consider these as well? (That is, comparison in EqualityTest, and maybe EqualsMethod call (qualifier or argument), where the compared value is a compile time constant)

(Feel free to consider this only as suggestion; I am not a member of this project)

java/ql/lib/semmle/code/java/StringCheck.qll Outdated Show resolved Hide resolved
java/ql/lib/semmle/code/java/os/OSCheck.qll Outdated Show resolved Hide resolved
@atorralba
Copy link
Contributor

atorralba commented Mar 11, 2022

This has some QLDoc errors:

The following CodeQL elements are lacking documentation:
semmle/code/java/environment/SystemProperty.qll  SystemProperty                   file
semmle/code/java/frameworks/Properties.qll       Properties::PropertiesGetMethod  class

Also, a bunch of autoformatting required:

Error: ql/java/ql/src/Security/CWE/CWE-200/TempDirLocalInformationDisclosure.ql would change by autoformatting.
Error: ql/java/ql/test/library-tests/os/unix-test.ql would change by autoformatting.
Error: ql/java/ql/test/library-tests/os/specific-unix-variant-test.ql would change by autoformatting.
Error: ql/java/ql/test/library-tests/os/windows-test.ql would change by autoformatting.
Error: ql/java/ql/test/library-tests/os/specific-windows-variant-test.ql would change by autoformatting.

@smowton
Copy link
Contributor

smowton commented Mar 11, 2022

Having to revert #8360 because of apparent performance problems. Remind me why you needed concatenation of string + char again?

If you still want it, a few possibilities to investigate:

  1. Using test project https://lgtm.com/projects/g/geogebra/geogebra, why do we end up performing millions of string concatenations at considerable cost? Are there really millions of string + non-string-constant operations in the codebase?
  2. If there really are some long concatenation sequences in the codebase causing this to cost a lot, consider a recursion that finds the parts of the concatenation and assigns them integer indices, before finally concatenating them in one go using QL's concat aggregate.

@JLLeitschuh
Copy link
Contributor Author

Remind me why you needed concatenation of string + char again?

For this particular change, I don't really, I wrote that new predicate to be more extensive than I needed so that it was generally more useful. In this particular PR, I just need to do equality checking between both string and characters because the new system property predicate sometimes returns a expression that has the type char.

Would adding the cached annotation to getStrigifiedValue and getString resolve the performance issues?

@smowton
Copy link
Contributor

smowton commented Mar 11, 2022

No, cached is only useful if we're recomputing a particular predicate repeatedly. Here one evaluation of it is expensive.

@JLLeitschuh
Copy link
Contributor Author

If there really are some long concatenation sequences in the codebase causing this to cost a lot, consider a recursion that finds the parts of the concatenation and assigns them integer indices, before finally concatenating them in one go using QL's concat aggregate.

I have no idea what this would look like unfortunately. I'd need some pointers on what the code would look like here.

@smowton
Copy link
Contributor

smowton commented Mar 11, 2022

Probably not worth the difficulty just for this. I've made #8407 to revert the original addition of CharacterLiteral to getStringValue per Marcono's objections in #8325, and recommend you simply special-case CharacterLiteral in this PR as you did originally for the time being. We can investigate how to do this in a performant manner as and when the need arises.

@JLLeitschuh
Copy link
Contributor Author

I'm ready for a final PR review or a merge if everyone is happy with this in it's current state. 😃

Copy link
Contributor

@smowton smowton left a comment

Choose a reason for hiding this comment

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

I believe we don't have tests for the Guava cases yet?

@smowton
Copy link
Contributor

smowton commented Mar 15, 2022

I also made a minor commit above to improve some class names.

@JLLeitschuh
Copy link
Contributor Author

I believe we don't have tests for the Guava cases yet?

Done

@JLLeitschuh
Copy link
Contributor Author

Anything else required here?

@smowton
Copy link
Contributor

smowton commented Mar 17, 2022

Just running a new performance evaluation as I realised the first one only considered queries with precision >= high, whereas the temp-dir-local-info query has precision = medium

@smowton smowton merged commit 7674535 into github:main Mar 18, 2022
@JLLeitschuh
Copy link
Contributor Author

Yeay! Very happy this is finally merged!

Comment on lines +111 to +125
private class IsNotUnixBarrierGuard extends WindowsOsBarrierGuard instanceof IsUnixGuard {
override predicate checks(Expr e, boolean branch) {
this.controls(e.getBasicBlock(), branch.booleanNot())
}
}

private class IsWindowsBarrierGuard extends WindowsOsBarrierGuard instanceof IsWindowsGuard {
override predicate checks(Expr e, boolean branch) { this.controls(e.getBasicBlock(), branch) }
}

private class IsSpecificWindowsBarrierGuard extends WindowsOsBarrierGuard instanceof IsSpecificWindowsVariant {
override predicate checks(Expr e, boolean branch) {
branch = true and this.controls(e.getBasicBlock(), branch)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

These three barrier-guards don't really make sense. Using Guard.controls in the checks predicate of a barrier-guard is almost guaranteed to be completely wrong, as the barrier-guard abstraction is explicitly designed to handle the .controls-aspect of guards.
The expression e should be the variable access being checked by the guard, and the branch should be the result of the guard that indicates that e is safe. So the basic implementation hint for the checks predicate should just be to use the AST and nothing more. If you really want to use Guard.controls to pick out the nodes that are sanitised then you're really just defining a regular sanitizer and not a sanitizer-guard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I partially understand what you're saying. That being said, I don't know how to fix it.

Also, could you suggest a test case that this doesn't work for that I could use as a unit test

Copy link
Contributor

@aschackmull aschackmull Apr 5, 2022

Choose a reason for hiding this comment

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

A barrierguard is just syntactic sugar for a sanitizer with a specific pattern. To see how a barrier-guard gets desugared into a sanitizer, look at the getAGuardedNode() predicate:

  final Node getAGuardedNode() {
    exists(SsaVariable v, boolean branch, RValue use |
      this.checks(v.getAUse(), branch) and
      use = v.getAUse() and
      this.controls(use.getBasicBlock(), branch) and
      result.asExpr() = use
    )
  }

You end up just joining this.controls with itself, which semantically is mostly harmless. But you can simplify things by just defining the sanitizers directly. E.g. replace IsSpecificWindowsBarrierGuard with the following:

abstract private class WindowsOsSanitizer extends DataFlow::Node { }

private class IsSpecificWindowsSanitizer extends WindowsOsSanitizer {
  IsSpecificWindowsSanitizer() {
    any(IsSpecificWindowsVariant guard).control(this.asExpr().getBasicBlock(), true)
  }
}

...

 override predicate isSanitizer(DataFlow::Node node) {
    node instanceof WindowsOsSanitizer
  }

This is mostly equivalent to your IsSpecificWindowsBarrierGuard except it drops the restriction that only uses of ssa variables are sanitized.
The other two, IsNotUnixBarrierGuard and IsWindowsBarrierGuard, can be similarly rewritten, but note that they are somewhat broken as-is, since they currently guard both of their branches. In

if (<IsWindowsBarrierGuard>) {
  foo
} else {
  bar
}

you're sanitizing both foo and bar.

private predicate isOsFromApacheCommons(FieldAccess fa, string fieldNamePattern) {
exists(Field f | f = fa.getField() |
f.getDeclaringType() instanceof TypeApacheSystemUtils and
f.getName().matches(fieldNamePattern)
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it is unlikely that this will give spurious matches, but do note that _ is a wildcard for the .matches predicate, so all of the underscores in the names below could be matched with anything.

JLLeitschuh added a commit to JLLeitschuh/ql that referenced this pull request Apr 6, 2022
JLLeitschuh added a commit to JLLeitschuh/ql that referenced this pull request Apr 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants