Merge pull request #21654 from MarkLee131/fix/sensitive-log-hash-sanitizer

Java: treat hash/encrypt/digest methods as sensitive-log sanitizers
This commit is contained in:
Owen Mansel-Chan
2026-04-30 13:21:03 +01:00
committed by GitHub
6 changed files with 73 additions and 43 deletions

View File

@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `java/sensitive-log` query now treats method calls whose names contain "encrypt", "hash", or "digest" as sanitizers, consistent with the existing treatment in `java/cleartext-storage-in-log`. This reduces false positives when sensitive data is hashed or encrypted before logging.

View File

@@ -2,6 +2,7 @@
import java
private import semmle.code.java.dataflow.TaintTracking
private import semmle.code.java.security.Sanitizers
private import semmle.code.java.security.SensitiveActions
/** A sink representing persistent storage that saves data in clear text. */
@@ -76,17 +77,6 @@ private class DefaultCleartextStorageSanitizer extends CleartextStorageSanitizer
}
}
/**
* Method call for encrypting sensitive information. As there are various implementations of
* encryption (reversible and non-reversible) from both JDK and third parties, this class simply
* checks method name to take a best guess to reduce false positives.
*/
private class EncryptedSensitiveMethodCall extends MethodCall {
EncryptedSensitiveMethodCall() {
this.getMethod().getName().toLowerCase().matches(["%encrypt%", "%hash%", "%digest%"])
}
}
/** Flow configuration for encryption methods flowing to inputs of persistent storage. */
private module EncryptedValueFlowConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node src) { src.asExpr() instanceof EncryptedSensitiveMethodCall }

View File

@@ -63,3 +63,14 @@ class RegexpCheckBarrier extends DataFlow::Node {
exists(RegexMatch rm | rm instanceof Annotation | this.asExpr() = rm.getString())
}
}
/**
* A method call for encrypting, hashing, or digesting sensitive information. As there are various
* implementations of encryption (reversible and non-reversible) from both JDK and third parties,
* this class simply checks the method name to take a best guess to reduce false positives.
*/
class EncryptedSensitiveMethodCall extends MethodCall {
EncryptedSensitiveMethodCall() {
this.getMethod().getName().toLowerCase().matches(["%encrypt%", "%hash%", "%digest%"])
}
}

View File

@@ -120,6 +120,14 @@ private class DefaultSensitiveLoggerBarrier extends SensitiveLoggerBarrier {
}
}
/**
* A barrier for sensitive data that has been hashed, encrypted, or digested before logging.
* This is consistent with the treatment of encryption in `CleartextStorageQuery.qll` (CWE-312).
*/
private class EncryptionBarrier extends SensitiveLoggerBarrier {
EncryptionBarrier() { this.asExpr() instanceof EncryptedSensitiveMethodCall }
}
/** A data-flow configuration for identifying potentially-sensitive data flowing to a log output. */
module SensitiveLoggerConfig implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof SensitiveLoggerSource }

View File

@@ -3,14 +3,15 @@
| Test.java:12:22:12:52 | ... + ... | Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | This $@ is written to a log file. | Test.java:12:44:12:52 | authToken | potentially sensitive information |
| Test.java:21:22:21:75 | ... + ... | Test.java:21:44:21:52 | authToken : String | Test.java:21:22:21:75 | ... + ... | This $@ is written to a log file. | Test.java:21:44:21:52 | authToken | potentially sensitive information |
| Test.java:22:22:22:75 | ... + ... | Test.java:22:44:22:52 | authToken : String | Test.java:22:22:22:75 | ... + ... | This $@ is written to a log file. | Test.java:22:44:22:52 | authToken | potentially sensitive information |
| Test.java:66:21:66:43 | ... + ... | Test.java:66:33:66:43 | accessToken : String | Test.java:66:21:66:43 | ... + ... | This $@ is written to a log file. | Test.java:66:33:66:43 | accessToken | potentially sensitive information |
| Test.java:67:21:67:45 | ... + ... | Test.java:67:34:67:45 | clientSecret : String | Test.java:67:21:67:45 | ... + ... | This $@ is written to a log file. | Test.java:67:34:67:45 | clientSecret | potentially sensitive information |
| Test.java:68:21:68:42 | ... + ... | Test.java:68:34:68:42 | apiSecret : String | Test.java:68:21:68:42 | ... + ... | This $@ is written to a log file. | Test.java:68:34:68:42 | apiSecret | potentially sensitive information |
| Test.java:69:21:69:44 | ... + ... | Test.java:69:33:69:44 | sessionToken : String | Test.java:69:21:69:44 | ... + ... | This $@ is written to a log file. | Test.java:69:33:69:44 | sessionToken | potentially sensitive information |
| Test.java:70:21:70:43 | ... + ... | Test.java:70:33:70:43 | bearerToken : String | Test.java:70:21:70:43 | ... + ... | This $@ is written to a log file. | Test.java:70:33:70:43 | bearerToken | potentially sensitive information |
| Test.java:71:21:71:39 | ... + ... | Test.java:71:31:71:39 | secretKey : String | Test.java:71:21:71:39 | ... + ... | This $@ is written to a log file. | Test.java:71:31:71:39 | secretKey | potentially sensitive information |
| Test.java:72:21:72:44 | ... + ... | Test.java:72:33:72:44 | refreshToken : String | Test.java:72:21:72:44 | ... + ... | This $@ is written to a log file. | Test.java:72:33:72:44 | refreshToken | potentially sensitive information |
| Test.java:73:21:73:43 | ... + ... | Test.java:73:33:73:43 | secretValue : String | Test.java:73:21:73:43 | ... + ... | This $@ is written to a log file. | Test.java:73:33:73:43 | secretValue | potentially sensitive information |
| Test.java:31:21:31:37 | ... + ... | Test.java:31:30:31:37 | password : String | Test.java:31:21:31:37 | ... + ... | This $@ is written to a log file. | Test.java:31:30:31:37 | password | potentially sensitive information |
| Test.java:75:21:75:43 | ... + ... | Test.java:75:33:75:43 | accessToken : String | Test.java:75:21:75:43 | ... + ... | This $@ is written to a log file. | Test.java:75:33:75:43 | accessToken | potentially sensitive information |
| Test.java:76:21:76:45 | ... + ... | Test.java:76:34:76:45 | clientSecret : String | Test.java:76:21:76:45 | ... + ... | This $@ is written to a log file. | Test.java:76:34:76:45 | clientSecret | potentially sensitive information |
| Test.java:77:21:77:42 | ... + ... | Test.java:77:34:77:42 | apiSecret : String | Test.java:77:21:77:42 | ... + ... | This $@ is written to a log file. | Test.java:77:34:77:42 | apiSecret | potentially sensitive information |
| Test.java:78:21:78:44 | ... + ... | Test.java:78:33:78:44 | sessionToken : String | Test.java:78:21:78:44 | ... + ... | This $@ is written to a log file. | Test.java:78:33:78:44 | sessionToken | potentially sensitive information |
| Test.java:79:21:79:43 | ... + ... | Test.java:79:33:79:43 | bearerToken : String | Test.java:79:21:79:43 | ... + ... | This $@ is written to a log file. | Test.java:79:33:79:43 | bearerToken | potentially sensitive information |
| Test.java:80:21:80:39 | ... + ... | Test.java:80:31:80:39 | secretKey : String | Test.java:80:21:80:39 | ... + ... | This $@ is written to a log file. | Test.java:80:31:80:39 | secretKey | potentially sensitive information |
| Test.java:81:21:81:44 | ... + ... | Test.java:81:33:81:44 | refreshToken : String | Test.java:81:21:81:44 | ... + ... | This $@ is written to a log file. | Test.java:81:33:81:44 | refreshToken | potentially sensitive information |
| Test.java:82:21:82:43 | ... + ... | Test.java:82:33:82:43 | secretValue : String | Test.java:82:21:82:43 | ... + ... | This $@ is written to a log file. | Test.java:82:33:82:43 | secretValue | potentially sensitive information |
edges
| Test.java:11:46:11:53 | password : String | Test.java:11:21:11:53 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:12:44:12:52 | authToken : String | Test.java:12:22:12:52 | ... + ... | provenance | Sink:MaD:1 |
@@ -18,14 +19,15 @@ edges
| Test.java:21:44:21:67 | substring(...) : String | Test.java:21:22:21:75 | ... + ... | provenance | Sink:MaD:1 |
| Test.java:22:44:22:52 | authToken : String | Test.java:22:44:22:67 | substring(...) : String | provenance | MaD:3 |
| Test.java:22:44:22:67 | substring(...) : String | Test.java:22:22:22:75 | ... + ... | provenance | Sink:MaD:1 |
| Test.java:66:33:66:43 | accessToken : String | Test.java:66:21:66:43 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:67:34:67:45 | clientSecret : String | Test.java:67:21:67:45 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:68:34:68:42 | apiSecret : String | Test.java:68:21:68:42 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:69:33:69:44 | sessionToken : String | Test.java:69:21:69:44 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:70:33:70:43 | bearerToken : String | Test.java:70:21:70:43 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:71:31:71:39 | secretKey : String | Test.java:71:21:71:39 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:72:33:72:44 | refreshToken : String | Test.java:72:21:72:44 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:73:33:73:43 | secretValue : String | Test.java:73:21:73:43 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:31:30:31:37 | password : String | Test.java:31:21:31:37 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:75:33:75:43 | accessToken : String | Test.java:75:21:75:43 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:76:34:76:45 | clientSecret : String | Test.java:76:21:76:45 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:77:34:77:42 | apiSecret : String | Test.java:77:21:77:42 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:78:33:78:44 | sessionToken : String | Test.java:78:21:78:44 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:79:33:79:43 | bearerToken : String | Test.java:79:21:79:43 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:80:31:80:39 | secretKey : String | Test.java:80:21:80:39 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:81:33:81:44 | refreshToken : String | Test.java:81:21:81:44 | ... + ... | provenance | Sink:MaD:2 |
| Test.java:82:33:82:43 | secretValue : String | Test.java:82:21:82:43 | ... + ... | provenance | Sink:MaD:2 |
models
| 1 | Sink: org.apache.logging.log4j; Logger; true; error; (String); ; Argument[0]; log-injection; manual |
| 2 | Sink: org.apache.logging.log4j; Logger; true; info; (String); ; Argument[0]; log-injection; manual |
@@ -41,20 +43,22 @@ nodes
| Test.java:22:22:22:75 | ... + ... | semmle.label | ... + ... |
| Test.java:22:44:22:52 | authToken : String | semmle.label | authToken : String |
| Test.java:22:44:22:67 | substring(...) : String | semmle.label | substring(...) : String |
| Test.java:66:21:66:43 | ... + ... | semmle.label | ... + ... |
| Test.java:66:33:66:43 | accessToken : String | semmle.label | accessToken : String |
| Test.java:67:21:67:45 | ... + ... | semmle.label | ... + ... |
| Test.java:67:34:67:45 | clientSecret : String | semmle.label | clientSecret : String |
| Test.java:68:21:68:42 | ... + ... | semmle.label | ... + ... |
| Test.java:68:34:68:42 | apiSecret : String | semmle.label | apiSecret : String |
| Test.java:69:21:69:44 | ... + ... | semmle.label | ... + ... |
| Test.java:69:33:69:44 | sessionToken : String | semmle.label | sessionToken : String |
| Test.java:70:21:70:43 | ... + ... | semmle.label | ... + ... |
| Test.java:70:33:70:43 | bearerToken : String | semmle.label | bearerToken : String |
| Test.java:71:21:71:39 | ... + ... | semmle.label | ... + ... |
| Test.java:71:31:71:39 | secretKey : String | semmle.label | secretKey : String |
| Test.java:72:21:72:44 | ... + ... | semmle.label | ... + ... |
| Test.java:72:33:72:44 | refreshToken : String | semmle.label | refreshToken : String |
| Test.java:73:21:73:43 | ... + ... | semmle.label | ... + ... |
| Test.java:73:33:73:43 | secretValue : String | semmle.label | secretValue : String |
| Test.java:31:21:31:37 | ... + ... | semmle.label | ... + ... |
| Test.java:31:30:31:37 | password : String | semmle.label | password : String |
| Test.java:75:21:75:43 | ... + ... | semmle.label | ... + ... |
| Test.java:75:33:75:43 | accessToken : String | semmle.label | accessToken : String |
| Test.java:76:21:76:45 | ... + ... | semmle.label | ... + ... |
| Test.java:76:34:76:45 | clientSecret : String | semmle.label | clientSecret : String |
| Test.java:77:21:77:42 | ... + ... | semmle.label | ... + ... |
| Test.java:77:34:77:42 | apiSecret : String | semmle.label | apiSecret : String |
| Test.java:78:21:78:44 | ... + ... | semmle.label | ... + ... |
| Test.java:78:33:78:44 | sessionToken : String | semmle.label | sessionToken : String |
| Test.java:79:21:79:43 | ... + ... | semmle.label | ... + ... |
| Test.java:79:33:79:43 | bearerToken : String | semmle.label | bearerToken : String |
| Test.java:80:21:80:39 | ... + ... | semmle.label | ... + ... |
| Test.java:80:31:80:39 | secretKey : String | semmle.label | secretKey : String |
| Test.java:81:21:81:44 | ... + ... | semmle.label | ... + ... |
| Test.java:81:33:81:44 | refreshToken : String | semmle.label | refreshToken : String |
| Test.java:82:21:82:43 | ... + ... | semmle.label | ... + ... |
| Test.java:82:33:82:43 | secretValue : String | semmle.label | secretValue : String |
subpaths

View File

@@ -22,6 +22,15 @@ class Test {
logger.error("Auth failed for: " + authToken.substring(0,8) + "..."); // $ Alert
}
// Tests for hash/encryption sanitizer
void testHashSanitizer(String password, String authToken) {
Logger logger = null;
logger.info("hash: " + hashPassword(password)); // Safe - hashed
logger.info("hash: " + sha256Digest(authToken)); // Safe - digested
logger.info("enc: " + encryptValue(password)); // Safe - encrypted
logger.info("pw: " + password); // $ Alert // not hashed
}
// Tests for false positive exclusions: variables with "token" or "secret" in the name
// that do not hold sensitive data.
void testFalsePositiveExclusions(
@@ -72,4 +81,8 @@ class Test {
logger.info("token: " + refreshToken); // $ Alert
logger.info("value: " + secretValue); // $ Alert
}
static String hashPassword(String input) { return input; }
static String sha256Digest(String input) { return input; }
static String encryptValue(String input) { return input; }
}