You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

178 lines
7.5 KiB
Java

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.
*
* <p>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.
}
}
}