diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb563817..d55a184c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,6 @@ jobs: build_and_test_cedar_java_ffi: name: Rust project - latest runs-on: ubuntu-latest - strategy: - matrix: - toolchain: - - stable steps: - name: Checkout Cedar Examples uses: actions/checkout@v3 @@ -24,7 +20,7 @@ jobs: ref: release/2.3.x path: ./cedar - name: rustup - run: rustup update ${{ matrix.toolchain }} && rustup default ${{ matrix.toolchain }} + run: rustup update stable && rustup default stable - name: cargo fmt working-directory: ./CedarJavaFFI run: cargo fmt --all --check @@ -34,7 +30,9 @@ jobs: run: bash config.sh run_int_tests - name: cargo rustc working-directory: ./CedarJavaFFI - run: RUSTFLAGS="-D warnings -F unsafe-code" cargo build --verbose + run: | + sed -i '1i #![allow(text_direction_codepoint_in_literal)]' ../cedar/cedar-policy-validator/src/lib.rs + cargo build --verbose - name: cargo test working-directory: ./CedarJavaFFI run: cargo test --verbose diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle index c2ed447f..b70777df 100644 --- a/CedarJava/build.gradle +++ b/CedarJava/build.gradle @@ -28,6 +28,9 @@ plugins { id 'signing' } +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 + /* Applies community Gradle plugins, usually added as build-tools in Config. */ @@ -38,11 +41,18 @@ apply plugin: 'com.github.spotbugs' apply plugin: 'org.owasp.dependencycheck' -check.dependsOn dependencyCheckAnalyze +if (System.getenv('NVD_API_KEY')) { + check.dependsOn dependencyCheckAnalyze +} dependencyCheck { format='HTML' failBuildOnCVSS=7 suppressionFile='suppressions.xml' + if (System.getenv('NVD_API_KEY')) { + nvd { + apiKey = System.getenv('NVD_API_KEY') + } + } } /* @@ -116,7 +126,7 @@ publishing { from components.java groupId = 'com.cedarpolicy' artifactId = 'cedar-java' - version = '2.3.3' + version = '2.3.6' pom { name = 'CedarJava' description = 'Java bindings for Cedar policy language.' diff --git a/CedarJava/config.sh b/CedarJava/config.sh old mode 100644 new mode 100755 index e2fc729f..f9b411c7 --- a/CedarJava/config.sh +++ b/CedarJava/config.sh @@ -14,8 +14,7 @@ if [ "$(uname)" == "Darwin" ]; then else ffi_lib_str=" environment 'CEDAR_JAVA_FFI_LIB', '"$parent_dir"/CedarJavaFFI/target/debug/libcedar_java_ffi.so'" fi -sed "101s;.*;$ffi_lib_str;" "build.gradle" > new_build.gradle -mv new_build.gradle build.gradle +sed -i.bak "s|.*environment 'CEDAR_JAVA_FFI_LIB'.*|$ffi_lib_str|" "build.gradle" && rm -f build.gradle.bak # In CI, we need to pull the latest cedar-policy to match the latest cedar-integration-tests # We require that integration tests be run @@ -23,8 +22,7 @@ mv new_build.gradle build.gradle # If you call this script with `run_int_tests`, we assume you have `cedar` checkout out in the `cedar-java` dir if [ "$#" -ne 0 ] && [ "$1" == "run_int_tests" ]; then integration_tests_str=" environment 'CEDAR_INTEGRATION_TESTS_ROOT', '"$parent_dir"/cedar/cedar-integration-tests'" - sed "100s;.*;$integration_tests_str;" "build.gradle" > new_build.gradle - mv new_build.gradle build.gradle + sed -i.bak "s|.*environment.*CEDAR_INTEGRATION_TESTS_ROOT.*|$integration_tests_str|" "build.gradle" && rm -f build.gradle.bak export MUST_RUN_CEDAR_INTEGRATION_TESTS=1 diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java index 7be6b02d..7aaebe1a 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueCedarSerializer.java @@ -68,7 +68,13 @@ public void serialize( } else if (value instanceof CedarMap) { jsonGenerator.writeStartObject(); for (Map.Entry entry : ((CedarMap) value).entrySet()) { - jsonGenerator.writeObjectField(entry.getKey(), entry.getValue()); + String key = entry.getKey(); + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new InvalidValueSerializationException( + "CedarMap key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + jsonGenerator.writeObjectField(key, entry.getValue()); } jsonGenerator.writeEndObject(); } else if (value instanceof IpAddress) { diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java b/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java index 316e878f..2dbe40d1 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/CedarMap.java @@ -25,6 +25,10 @@ /** Represents a Cedar Map value. Maps support mapping strings to arbitrary values. */ public final class CedarMap extends Value implements Map { + /** Reserved keys in the Cedar JSON protocol that cannot be used as record keys. */ + private static final String ENTITY_ESCAPE_SEQ = "__entity"; + private static final String EXTENSION_ESCAPE_SEQ = "__extn"; + /** Internal map data. */ private final java.util.Map map; @@ -34,6 +38,13 @@ public final class CedarMap extends Value implements Map { * @param source map to copy from */ public CedarMap(java.util.Map source) { + for (String key : source.keySet()) { + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new IllegalArgumentException( + "Key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + } this.map = new HashMap<>(source); } @@ -66,7 +77,7 @@ public int hashCode() { public String toCedarExpr() { return "{" + map.entrySet().stream() - .map(e -> '\"' + e.getKey() + "\": " + e.getValue().toCedarExpr()) + .map(e -> '\"' + CedarStrings.escape(e.getKey()) + "\": " + e.getValue().toCedarExpr()) .collect(Collectors.joining(", ")) + "}"; } @@ -118,12 +129,24 @@ public Value put(String k, Value v) throws NullPointerException { if (v == null) { throw new NullPointerException("Attempt to put null value in CedarMap"); } + if (ENTITY_ESCAPE_SEQ.equals(k) || EXTENSION_ESCAPE_SEQ.equals(k)) { + throw new IllegalArgumentException( + "Key \"" + k + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } return map.put(k, v); } @Override public void putAll(Map m) { + for (String key : m.keySet()) { + if (ENTITY_ESCAPE_SEQ.equals(key) || EXTENSION_ESCAPE_SEQ.equals(key)) { + throw new IllegalArgumentException( + "Key \"" + key + "\" is reserved by the Cedar JSON protocol" + + " and cannot be used as a record key."); + } + } map.putAll(m); } diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java b/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java new file mode 100644 index 00000000..5f954033 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/value/CedarStrings.java @@ -0,0 +1,63 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy.value; + +/** + * Utility methods for working with Cedar string literals. + */ +public final class CedarStrings { + + private CedarStrings() { } + + /** + * Escape a string for safe inclusion in Cedar source code as a string literal. + * Handles backslashes, double quotes, and control characters recognized by Cedar. + * + * @param s the raw string value + * @return the escaped string (without surrounding quotes) + */ + public static String escape(String s) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\': + sb.append("\\\\"); + break; + case '"': + sb.append("\\\""); + break; + case '\n': + sb.append("\\n"); + break; + case '\r': + sb.append("\\r"); + break; + case '\t': + sb.append("\\t"); + break; + case '\0': + sb.append("\\0"); + break; + default: + sb.append(c); + break; + } + } + return sb.toString(); + } +} diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java b/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java index c3419d2a..ed9533a9 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/PrimString.java @@ -60,6 +60,6 @@ public String toString() { /** To Cedar expr that can be used in a Cedar policy. */ @Override public String toCedarExpr() { - return "\"" + value + "\""; + return "\"" + CedarStrings.escape(value) + "\""; } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java b/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java new file mode 100644 index 00000000..d11ea45e --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/CedarExprEscapingTests.java @@ -0,0 +1,72 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.cedarpolicy.value.CedarMap; +import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Value; + +public class CedarExprEscapingTests { + + @Test + public void testPrimStringEscapesDoubleQuotes() { + PrimString s = new PrimString("x\" && false && \"x"); + assertEquals("\"x\\\" && false && \\\"x\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEscapesBackslash() { + PrimString s = new PrimString("path\\to\\file"); + assertEquals("\"path\\\\to\\\\file\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEscapesControlCharacters() { + PrimString s = new PrimString("line1\nline2\ttab\r\0"); + assertEquals("\"line1\\nline2\\ttab\\r\\0\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringPlainStringUnchanged() { + PrimString s = new PrimString("hello world"); + assertEquals("\"hello world\"", s.toCedarExpr()); + } + + @Test + public void testPrimStringEmptyString() { + PrimString s = new PrimString(""); + assertEquals("\"\"", s.toCedarExpr()); + } + + @Test + public void testCedarMapEscapesKeys() { + Map source = new HashMap<>(); + source.put("key\"injection", new PrimString("value")); + CedarMap map = new CedarMap(source); + String expr = map.toCedarExpr(); + assertTrue(expr.contains("key\\\"injection")); + assertFalse(expr.contains("key\"injection")); + } + +} diff --git a/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java b/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java new file mode 100644 index 00000000..9706ac09 --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/CedarMapReservedKeyTests.java @@ -0,0 +1,96 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.cedarpolicy.value.CedarMap; +import com.cedarpolicy.value.PrimString; +import com.cedarpolicy.value.Value; + +public class CedarMapReservedKeyTests { + + @Test + public void testPutRejectsEntityKey() { + CedarMap map = new CedarMap(); + assertThrows(IllegalArgumentException.class, () -> { + map.put("__entity", new PrimString("spoofed")); + }); + } + + @Test + public void testPutRejectsExtnKey() { + CedarMap map = new CedarMap(); + assertThrows(IllegalArgumentException.class, () -> { + map.put("__extn", new PrimString("spoofed")); + }); + } + + @Test + public void testConstructorRejectsEntityKey() { + Map source = new HashMap<>(); + source.put("__entity", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + new CedarMap(source); + }); + } + + @Test + public void testConstructorRejectsExtnKey() { + Map source = new HashMap<>(); + source.put("__extn", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + new CedarMap(source); + }); + } + + @Test + public void testPutAllRejectsEntityKey() { + CedarMap map = new CedarMap(); + Map source = new HashMap<>(); + source.put("safe", new PrimString("ok")); + source.put("__entity", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + map.putAll(source); + }); + } + + @Test + public void testPutAllRejectsExtnKey() { + CedarMap map = new CedarMap(); + Map source = new HashMap<>(); + source.put("__extn", new PrimString("spoofed")); + assertThrows(IllegalArgumentException.class, () -> { + map.putAll(source); + }); + } + + @Test + public void testNormalKeysStillWork() { + CedarMap map = new CedarMap(); + assertDoesNotThrow(() -> { + map.put("entity", new PrimString("ok")); + map.put("__other", new PrimString("ok")); + map.put("_entity", new PrimString("ok")); + }); + } +}