package com.razz.dfashion.security; import com.code_intelligence.jazzer.junit.FuzzTest; import com.google.gson.JsonParseException; import com.razz.dfashion.bbmodel.BbModelParser; import org.junit.jupiter.api.Test; import java.io.StringReader; import static org.junit.jupiter.api.Assertions.*; /** * Security corpus + fuzz harness for {@link BbModelParser}. The parser runs only on the * author's local disk, but still handles untrusted JSON (the author could have downloaded * a malicious .bbmodel), so every rejection rule matters. * *

Fuzz inputs are fed as UTF-8 byte arrays. Legitimate rejections are any subtype of * {@link JsonParseException} (including {@code BadBbmodelException}). A {@link StackOverflowError}, * {@link NullPointerException}, or any non-JsonParseException is a bug Jazzer should surface. */ class BbModelParserSecurityTest { @Test void acceptsMinimalValidModel() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[]," + "\"groups\":[]," + "\"outliner\":[]" + "}"; assertDoesNotThrow(() -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsOldFormatVersion() { String json = "{" + "\"meta\":{\"format_version\":\"4.5\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[],\"groups\":[],\"outliner\":[]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsResolutionOverCap() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":" + (BbModelParser.MAX_RESOLUTION + 1) + ",\"height\":64}," + "\"elements\":[],\"groups\":[],\"outliner\":[]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsNanFloat() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[{\"type\":\"cube\",\"uuid\":\"a\",\"name\":\"n\"," + "\"from\":[\"NaN\",0,0],\"to\":[1,1,1],\"origin\":[0,0,0],\"rotation\":[0,0,0]," + "\"inflate\":0,\"faces\":{}}]," + "\"groups\":[],\"outliner\":[\"a\"]" + "}"; // GSON parses NaN-as-string into Float.NaN (depending on lenient mode); post-parse // validation rejects non-finite floats. Either way, a JsonParseException is expected. assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsInfinityFloat() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[{\"type\":\"cube\",\"uuid\":\"a\",\"name\":\"n\"," + "\"from\":[0,0,0],\"to\":[1,1,1],\"origin\":[0,0,0]," + "\"rotation\":[\"Infinity\",0,0]," + "\"inflate\":0,\"faces\":{}}]," + "\"groups\":[],\"outliner\":[\"a\"]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsCoordOutOfRange() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[{\"type\":\"cube\",\"uuid\":\"a\",\"name\":\"n\"," + "\"from\":[1e9,0,0],\"to\":[1,1,1],\"origin\":[0,0,0],\"rotation\":[0,0,0]," + "\"inflate\":0,\"faces\":{}}]," + "\"groups\":[],\"outliner\":[\"a\"]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsDuplicateElementUuid() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[" + " {\"type\":\"cube\",\"uuid\":\"dup\",\"name\":\"n\"," + " \"from\":[0,0,0],\"to\":[1,1,1],\"origin\":[0,0,0],\"rotation\":[0,0,0],\"inflate\":0,\"faces\":{}}," + " {\"type\":\"cube\",\"uuid\":\"dup\",\"name\":\"n\"," + " \"from\":[0,0,0],\"to\":[1,1,1],\"origin\":[0,0,0],\"rotation\":[0,0,0],\"inflate\":0,\"faces\":{}}" + "]," + "\"groups\":[],\"outliner\":[\"dup\"]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsOutlinerRefToUnknownElement() { String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[]," + "\"groups\":[]," + "\"outliner\":[\"ghost-uuid\"]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsOutlinerOverDepthCap() { // Build a chain of groups deeper than MAX_OUTLINER_DEPTH. Each group is a GroupRef // whose single child is the next GroupRef. int depth = BbModelParser.MAX_OUTLINER_DEPTH + 5; StringBuilder groups = new StringBuilder(); StringBuilder outliner = new StringBuilder(); for (int i = 0; i < depth; i++) { if (i > 0) groups.append(','); groups.append("{\"uuid\":\"g").append(i).append("\",\"name\":\"g\"," + "\"origin\":[0,0,0],\"rotation\":[0,0,0]}"); } // Outliner is a nested object tree referencing g0 → g1 → ... → g(depth-1). for (int i = 0; i < depth; i++) { outliner.append("{\"uuid\":\"g").append(i).append("\",\"children\":["); } for (int i = 0; i < depth; i++) outliner.append("]}"); String json = "{" + "\"meta\":{\"format_version\":\"5.0\"}," + "\"resolution\":{\"width\":64,\"height\":64}," + "\"elements\":[]," + "\"groups\":[" + groups + "]," + "\"outliner\":[" + outliner + "]" + "}"; assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader(json))); } @Test void rejectsMalformedJson() { assertThrows(JsonParseException.class, () -> BbModelParser.parse(new StringReader("{ not json"))); } @Test void rejectsEmpty() { assertThrows(Exception.class, () -> BbModelParser.parse(new StringReader(""))); } // ---- fuzz harness ---- /** * Fuzz the JSON parser with random UTF-8. Legit rejection = anything JsonParseException-ish. * Other exceptions point at missing defense in the parser. */ @FuzzTest(maxDuration = "30s") void fuzzParse(byte[] input) { try { BbModelParser.parse(new StringReader(new String(input, java.nio.charset.StandardCharsets.UTF_8))); } catch (JsonParseException | IllegalStateException | NumberFormatException expected) { // All legitimate rejection paths from GSON + validator. } } }