Skip to content

Commit 9b62f2c

Browse files
authored
Merge pull request #3 from serj/annotation-extension
Annotation extension
2 parents 4fa1c6e + c724061 commit 9b62f2c

12 files changed

Lines changed: 2311 additions & 691 deletions

File tree

.idea/encodings.xml

Lines changed: 1 addition & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

agent/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,37 @@ mvn clean package -pl agent -am
7070

7171
The shaded JAR is at `agent/target/jcapslock-agent-1.0-SNAPSHOT.jar`.
7272

73+
## DEFAULT Blocking Mode
74+
75+
Use `DEFAULT` in the blocked list to block all capabilities NOT in the snapshot for a package:
76+
77+
```yaml
78+
policies:
79+
- package: com.example.myapp
80+
blocked:
81+
- DEFAULT # blocks all capabilities not in snapshot.json
82+
```
83+
84+
The agent loads `.capslock/snapshot.json` (relative to policy file) and allows only
85+
capabilities that exist in the snapshot for that package. Any capability usage not
86+
in the snapshot is blocked.
87+
88+
## Limitations
89+
90+
> **Proof of Concept**: The enforcement mode is experimental and can be evaded.
91+
> It should be used as one layer in a defense-in-depth strategy, not as a
92+
> complete security solution.
93+
94+
Known evasion vectors:
95+
- **Async/threaded code**: Stack inspection won't show original caller after handoff to thread pool
96+
- **Reflection**: Dynamically invoked methods bypass static analysis
97+
- **Native code**: JNI and `sun.misc.Unsafe` can perform any operation
98+
- **External processes**: Code executed via `Runtime.exec` is not monitored
99+
- **Class loading tricks**: Custom classloaders can load uninstrumented code
100+
101+
The static analysis also has inherent limitations:
102+
- Reflection targets cannot be determined statically and lead to overapproximations
103+
73104
## Capabilities
74105

75106
Capabilities are loaded from `java-interesting.cm` in the core module.

agent/src/main/java/com/github/serj/jcapslock/agent/CapslockAgent.java

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package com.github.serj.jcapslock.agent;
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import com.fasterxml.jackson.databind.JsonNode;
45
import com.fasterxml.jackson.databind.ObjectMapper;
56
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
67
import org.objectweb.asm.Type;
78

89
import java.io.File;
910
import java.lang.instrument.Instrumentation;
11+
import java.util.Arrays;
1012
import java.util.HashMap;
1113
import java.util.HashSet;
1214
import java.util.List;
@@ -73,6 +75,24 @@ public static Instrumentation getInstrumentation() {
7375
return instrumentation;
7476
}
7577

78+
/**
79+
* All enforceable capabilities (excludes UNSPECIFIED, SAFE, UNANALYZED).
80+
*/
81+
private static final Set<String> ALL_CAPABILITIES = new HashSet<>(Arrays.asList(
82+
"CAPABILITY_FILES",
83+
"CAPABILITY_NETWORK",
84+
"CAPABILITY_RUNTIME",
85+
"CAPABILITY_READ_SYSTEM_STATE",
86+
"CAPABILITY_MODIFY_SYSTEM_STATE",
87+
"CAPABILITY_OPERATING_SYSTEM",
88+
"CAPABILITY_SYSTEM_CALLS",
89+
"CAPABILITY_ARBITRARY_EXECUTION",
90+
"CAPABILITY_CGO",
91+
"CAPABILITY_UNSAFE_POINTER",
92+
"CAPABILITY_REFLECT",
93+
"CAPABILITY_EXEC"
94+
));
95+
7696
private static void loadPolicy(String policyPath) {
7797
File policyFile = new File(policyPath);
7898
if (!policyFile.exists()) {
@@ -86,14 +106,42 @@ private static void loadPolicy(String policyPath) {
86106
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
87107
PolicyFile policy = mapper.readValue(policyFile, PolicyFile.class);
88108

109+
// Check if any policy uses DEFAULT - if so, load snapshot
110+
Map<String, Set<String>> snapshotCapabilities = null;
111+
boolean hasDefault = policy.policies != null && policy.policies.stream()
112+
.anyMatch(e -> e.blocked != null && e.blocked.contains("DEFAULT"));
113+
114+
if (hasDefault) {
115+
snapshotCapabilities = loadSnapshotCapabilities(policyFile.getParentFile());
116+
}
117+
89118
Map<String, Set<String>> capabilityToPackages = new HashMap<>();
90119
if (policy.policies != null) {
91120
for (PolicyEntry entry : policy.policies) {
92121
if (entry.blocked != null) {
93-
for (String capability : entry.blocked) {
94-
capabilityToPackages
95-
.computeIfAbsent(capability, k -> new HashSet<>())
96-
.add(entry.pkg);
122+
List<String> effectiveBlocked = entry.blocked;
123+
124+
// Expand DEFAULT to all capabilities not in snapshot for this package
125+
if (entry.blocked.contains("DEFAULT")) {
126+
Set<String> allowedFromSnapshot = snapshotCapabilities != null
127+
? snapshotCapabilities.getOrDefault(entry.pkg, Set.of())
128+
: Set.of();
129+
130+
Set<String> toBlock = new HashSet<>(ALL_CAPABILITIES);
131+
toBlock.removeAll(allowedFromSnapshot);
132+
effectiveBlocked = List.copyOf(toBlock);
133+
134+
Log.info("DEFAULT expansion for " + entry.pkg +
135+
": allowed from snapshot=" + allowedFromSnapshot +
136+
", blocking=" + toBlock);
137+
}
138+
139+
for (String capability : effectiveBlocked) {
140+
if (!"DEFAULT".equals(capability)) {
141+
capabilityToPackages
142+
.computeIfAbsent(capability, k -> new HashSet<>())
143+
.add(entry.pkg);
144+
}
97145
}
98146
}
99147
}
@@ -110,6 +158,42 @@ private static void loadPolicy(String policyPath) {
110158
}
111159
}
112160

161+
/**
162+
* Loads capabilities from snapshot.json in the given directory.
163+
* Returns a map from package name to set of capabilities.
164+
*/
165+
private static Map<String, Set<String>> loadSnapshotCapabilities(File directory) {
166+
File snapshotFile = new File(directory, "snapshot.json");
167+
if (!snapshotFile.exists()) {
168+
Log.warn("Snapshot file not found: " + snapshotFile + " (DEFAULT blocking may not work correctly)");
169+
return Map.of();
170+
}
171+
172+
try {
173+
ObjectMapper mapper = new ObjectMapper();
174+
JsonNode root = mapper.readTree(snapshotFile);
175+
JsonNode capabilityInfoList = root.get("capability_info");
176+
177+
Map<String, Set<String>> result = new HashMap<>();
178+
if (capabilityInfoList != null && capabilityInfoList.isArray()) {
179+
for (JsonNode info : capabilityInfoList) {
180+
String packageName = info.has("package_name") ? info.get("package_name").asText() : null;
181+
String capability = info.has("capability") ? info.get("capability").asText() : null;
182+
183+
if (packageName != null && capability != null) {
184+
result.computeIfAbsent(packageName, k -> new HashSet<>()).add(capability);
185+
}
186+
}
187+
}
188+
189+
Log.info("Loaded snapshot with " + result.size() + " packages");
190+
return result;
191+
} catch (Exception e) {
192+
Log.error("Failed to load snapshot: " + e.getMessage());
193+
return Map.of();
194+
}
195+
}
196+
113197
public static class PolicyFile {
114198
public List<PolicyEntry> policies;
115199
}

agent/src/main/java/com/github/serj/jcapslock/agent/PolicyChecker.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,37 @@ public class PolicyChecker {
1313

1414
public static final String PROP_PREFIX = "capslock.block.";
1515

16+
/**
17+
* Reentrancy guard to prevent infinite recursion when instrumented methods
18+
* (like System.getProperty, Thread.getStackTrace) are called from within check().
19+
* NOTE: Cannot use ThreadLocal.withInitial(() -> false) because lambda creation
20+
* triggers instrumented methods before IN_CHECK is initialized.
21+
*/
22+
private static final ThreadLocal<Boolean> IN_CHECK = new ThreadLocal<Boolean>() {
23+
@Override
24+
protected Boolean initialValue() {
25+
return Boolean.FALSE;
26+
}
27+
};
28+
1629
/**
1730
* Check if capability is allowed for the current call stack.
1831
* Throws SecurityException if blocked.
1932
*/
2033
public static void check(String capability) {
34+
// Reentrancy guard: if we're already in a check, skip to prevent infinite recursion
35+
if (IN_CHECK.get()) {
36+
return;
37+
}
38+
IN_CHECK.set(true);
39+
try {
40+
doCheck(capability);
41+
} finally {
42+
IN_CHECK.set(false);
43+
}
44+
}
45+
46+
private static void doCheck(String capability) {
2147
String blocked = System.getProperty(PROP_PREFIX + capability);
2248
String[] blockedPackages = (blocked != null) ? blocked.split(",") : null;
2349

@@ -31,21 +57,24 @@ public static void check(String capability) {
3157
RuntimeCallGraph.recordPath(capability, appFrames);
3258
}
3359

34-
logFrames(capability, appFrames);
35-
3660
if (blockedPackages != null) {
3761
String violator = findViolator(appFrames, blockedPackages);
3862
if (violator != null) {
63+
logFrames(capability, appFrames);
3964
throw new SecurityException("[CAPSLOCK] " + capability + " blocked for " + violator);
4065
}
4166
}
4267
}
4368

69+
private static final String POLICY_CHECKER_CLASS = PolicyChecker.class.getName();
70+
4471
private static List<StackTraceElement> collectAppFrames() {
4572
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
4673
List<StackTraceElement> appFrames = new ArrayList<>();
4774
for (int i = 2; i < stack.length; i++) {
48-
if (!JavaUtils.isJdkPackage(stack[i].getClassName())) {
75+
String className = stack[i].getClassName();
76+
// Filter out JDK packages and PolicyChecker itself (to avoid self-reference in logs)
77+
if (!JavaUtils.isJdkPackage(className) && !className.equals(POLICY_CHECKER_CLASS)) {
4978
appFrames.add(stack[i]);
5079
}
5180
}

0 commit comments

Comments
 (0)