Skip to content

Commit d041e2a

Browse files
NicolaiSoeborgggrossetie
authored andcommitted
feat(security): activate Structurizr restricted mode
Enable restricted parsing to disable `!script` and other dangerous methods to be executed during parsing By default, Kroki will parse Structurizr diagrams in "restricted mode" unless `KROKI_STRUCTURIZR_SAFE_MODE` (or `KROKI_SAFE_MODE`) is set to `unsafe`.
1 parent 65e9231 commit d041e2a

File tree

6 files changed

+99
-31
lines changed

6 files changed

+99
-31
lines changed

Diff for: docs/modules/setup/pages/configuration.adoc

+11-1
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ By default, Kroki is running in `SECURE` mode.
5959
====
6060
Some diagram libraries allow referencing external entities by URL or accessing resources from the filesystem.
6161
62-
For example PlantUML allows the `!import` directive to pull fragments from the filesystem or a remote URL or the standard library.
62+
For example, PlantUML allows the `!import` directive to pull fragments from the filesystem or a remote URL or the standard library.
6363
6464
It is the responsibility of the upstream codebases to ensure that they can be safely used without risk.
6565
Because Kroki does not perform code review of these services, our default setting is to be paranoid and block imports unless known safe.
6666
We encourage anyone running their own Kroki server to review the services security settings and select the security mode appropriate for their use case.
6767
====
6868

69+
=== PlantUML
70+
6971
While running in `SECURE` mode, Kroki will prevent PlantUML from including files using the `!include` or `!includeurl` directive.
7072

7173
If you want to enable this feature, you can set the safe mode using the environment variable `KROKI_SAFE_MODE`:
@@ -82,6 +84,14 @@ KROKI_PLANTUML_INCLUDE_WHITELIST:: The name of a file that consists of a list of
8284
KROKI_PLANTUML_INCLUDE_WHITELIST_0, KROKI_PLANTUML_INCLUDE_WHITELIST_1, ... KROKI_PLANTUML_INCLUDE_WHITELIST___N__:: One regex to add to the include whitelist per environment variable. Search will stop at the first empty or undefined integer number.
8385
KROKI_PLANTUML_ALLOW_INCLUDE:: Either `false` (default) or `true`. Determines if PlantUML will fetch `!include` directives that reference external URLs. For example, PlantUML allows the !import directive to pull fragments from the filesystem or a remote URL or the standard library.
8486

87+
=== Structurizr
88+
89+
Structurizr's restricted mode is activated unless Kroki is running in `UNSAFE` mode:
90+
91+
> Run this parser in restricted mode (this stops `!include`, `!docs`, `!adrs` from working).
92+
93+
If you want to enable this feature, you can set the safe mode using the global environment variable `KROKI_SAFE_MODE` or the specific environment variable `KROKI_STRUCTURIZR_SAFE_MODE` (i.e., the safe mode will only apply to Structurizr).
94+
8595
== Cross-origin resource sharing (CORS)
8696

8797
By default, the following headers are allowed:

Diff for: server/src/main/java/io/kroki/server/service/Structurizr.java

+16-11
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import io.kroki.server.error.BadRequestException;
1111
import io.kroki.server.error.DecodeException;
1212
import io.kroki.server.format.FileFormat;
13+
import io.kroki.server.security.SafeMode;
1314
import io.vertx.core.AsyncResult;
1415
import io.vertx.core.Handler;
1516
import io.vertx.core.Vertx;
@@ -23,12 +24,14 @@
2324
import java.io.InputStream;
2425
import java.io.InputStreamReader;
2526
import java.util.*;
27+
import java.util.concurrent.Callable;
2628
import java.util.stream.Collectors;
2729

2830
public class Structurizr implements DiagramService {
2931

3032
private final Vertx vertx;
3133
private final StructurizrPlantUMLExporter structurizrPlantUMLExporter;
34+
private final SafeMode safeMode;
3235
private final SourceDecoder sourceDecoder;
3336
private final PlantumlCommand plantumlCommand;
3437

@@ -48,6 +51,7 @@ public class Structurizr implements DiagramService {
4851

4952
public Structurizr(Vertx vertx, JsonObject config) {
5053
this.vertx = vertx;
54+
this.safeMode = SafeMode.get(config.getString("KROKI_STRUCTURIZR_SAFE_MODE", config.getString("KROKI_SAFE_MODE", "secure")), SafeMode.SECURE);
5155
this.structurizrPlantUMLExporter = new StructurizrPlantUMLExporter();
5256
this.sourceDecoder = new SourceDecoder() {
5357
@Override
@@ -75,19 +79,20 @@ public String getVersion() {
7579

7680
@Override
7781
public void convert(String sourceDecoded, String serviceName, FileFormat fileFormat, JsonObject options, Handler<AsyncResult<Buffer>> handler) {
78-
vertx.executeBlocking(future -> {
79-
try {
80-
byte[] data = convert(sourceDecoded, fileFormat, options);
81-
future.complete(data);
82-
} catch (Exception e) {
83-
future.fail(e);
84-
}
85-
}, res -> handler.handle(res.map(o -> Buffer.buffer((byte[]) o))));
82+
vertx.executeBlocking(() -> convert(sourceDecoded, fileFormat, options), res -> handler.handle(res.map(Buffer::buffer)));
8683
}
8784

88-
static byte[] convert(String source, FileFormat fileFormat, PlantumlCommand plantumlCommand, StructurizrPlantUMLExporter structurizrPlantUMLExporter, JsonObject options) throws IOException, InterruptedException {
85+
static byte[] convert(
86+
String source,
87+
FileFormat fileFormat,
88+
PlantumlCommand plantumlCommand,
89+
StructurizrPlantUMLExporter structurizrPlantUMLExporter,
90+
SafeMode safeMode,
91+
JsonObject options
92+
) throws IOException, InterruptedException {
8993
StructurizrDslParser parser = new StructurizrDslParser();
9094
try {
95+
parser.setRestricted(safeMode != SafeMode.UNSAFE);
9196
parser.parse(source);
9297
ViewSet viewSet = parser.getWorkspace().getViews();
9398
Collection<View> views = viewSet.getViews();
@@ -137,7 +142,7 @@ static byte[] convert(String source, FileFormat fileFormat, PlantumlCommand plan
137142
if (outputOption != null) {
138143
outputOption = outputOption.trim();
139144
}
140-
145+
141146
String diagramPlantUML;
142147
if (outputOption == null || outputOption.equals("diagram")) {
143148
diagramPlantUML = diagram.getDefinition();
@@ -161,7 +166,7 @@ static byte[] convert(String source, FileFormat fileFormat, PlantumlCommand plan
161166
}
162167

163168
private byte[] convert(String source, FileFormat fileFormat, JsonObject options) throws IOException, InterruptedException {
164-
return convert(source, fileFormat, this.plantumlCommand, this.structurizrPlantUMLExporter, options);
169+
return convert(source, fileFormat, this.plantumlCommand, this.structurizrPlantUMLExporter, this.safeMode, options);
165170
}
166171

167172
private static void applyTheme(ViewSet viewSet, StructurizrTheme theme) {

Diff for: server/src/test/java/io/kroki/server/service/StructurizrServiceTest.java

+47-19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import io.kroki.server.DownloadPlantumlNativeImage;
55
import io.kroki.server.error.BadRequestException;
66
import io.kroki.server.format.FileFormat;
7+
import io.kroki.server.security.SafeMode;
78
import io.vertx.core.Vertx;
89
import io.vertx.core.json.JsonObject;
910
import io.vertx.junit5.Checkpoint;
@@ -25,6 +26,7 @@
2526
import java.io.InputStreamReader;
2627
import java.nio.file.Files;
2728
import java.nio.file.Paths;
29+
import java.util.Objects;
2830
import java.util.stream.Collectors;
2931

3032
import static org.assertj.core.api.Assertions.assertThat;
@@ -64,7 +66,7 @@ public void should_convert_getting_started_example(String output) throws IOExcep
6466
options.put("output", output);
6567
}
6668

67-
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
69+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options);
6870
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
6971
} else {
7072
logger.info("/usr/bin/dot not found, skipping test.");
@@ -78,7 +80,7 @@ public void should_convert_bigbank_example_container_view() throws IOException,
7880
String expected = read("./bigbank.containers.expected.svg");
7981
JsonObject options = new JsonObject();
8082
options.put("view-key", "Containers");
81-
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
83+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options);
8284
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
8385
} else {
8486
logger.info("/usr/bin/dot not found, skipping test.");
@@ -92,7 +94,7 @@ public void should_convert_bigbank_example_systemcontext_view() throws IOExcepti
9294
String expected = read("./bigbank.systemcontext.expected.svg");
9395
JsonObject options = new JsonObject();
9496
options.put("view-key", "SystemContext");
95-
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
97+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options);
9698
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
9799
} else {
98100
logger.info("/usr/bin/dot not found, skipping test.");
@@ -107,7 +109,7 @@ public void should_convert_bigbank_example_systemcontext_legend() throws IOExcep
107109
JsonObject options = new JsonObject();
108110
options.put("view-key", "SystemContext");
109111
options.put("output", "legend");
110-
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
112+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options);
111113
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
112114
} else {
113115
logger.info("/usr/bin/dot not found, skipping test.");
@@ -119,7 +121,32 @@ public void should_convert_aws_example() throws IOException, InterruptedExceptio
119121
if (Files.isExecutable(Paths.get("/usr/bin/dot"))) {
120122
String source = read("./aws.structurizr");
121123
String expected = read("./aws.expected.svg");
122-
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), new JsonObject());
124+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, new JsonObject());
125+
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
126+
} else {
127+
logger.info("/usr/bin/dot not found, skipping test.");
128+
}
129+
}
130+
131+
@Test
132+
public void should_convert_docs_example() throws IOException {
133+
if (Files.isExecutable(Paths.get("/usr/bin/dot"))) {
134+
String source = read("docs.structurizr");
135+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, new JsonObject()))
136+
.isInstanceOf(BadRequestException.class)
137+
.hasMessageStartingWith("Unable to parse the Structurizr DSL. !docs is not available when the parser is running in restricted mode at line 5: !docs src/test/resources/docs");
138+
} else {
139+
logger.info("/usr/bin/dot not found, skipping test.");
140+
}
141+
}
142+
143+
@Test
144+
public void should_convert_docs_example_unsafe() throws IOException, InterruptedException {
145+
if (Files.isExecutable(Paths.get("/usr/bin/dot"))) {
146+
String source = read("docs.structurizr");
147+
String expected = read("./docs.expected.svg");
148+
// "docs" is not included when using the PlantUML exporter
149+
byte[] result = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.UNSAFE, new JsonObject());
123150
assertThat(stripComments(new String(result))).isEqualToIgnoringNewLines(expected);
124151
} else {
125152
logger.info("/usr/bin/dot not found, skipping test.");
@@ -131,51 +158,52 @@ public void should_throw_exception_when_view_does_not_exist() throws IOException
131158
String source = read("./bigbank.structurizr");
132159
JsonObject options = new JsonObject();
133160
options.put("view-key", "NonExisting");
134-
assertThatThrownBy(() -> {
135-
Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
136-
})
161+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options))
137162
.isInstanceOf(BadRequestException.class)
138163
.hasMessage("Unable to find view for key: NonExisting.");
139164
}
140165

141166
@Test
142167
public void should_throw_exception_when_diagram_is_empty() throws IOException {
143168
String source = read("./no-view.structurizr");
144-
assertThatThrownBy(() -> {
145-
Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), new JsonObject());
146-
})
169+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, new JsonObject()))
147170
.isInstanceOf(BadRequestException.class)
148171
.hasMessage("Empty diagram, does not have any view.");
149172
}
150173

151174
@Test
152175
public void should_throw_exception_when_script_directive_used() throws IOException {
153176
String source = read("./script.structurizr");
154-
assertThatThrownBy(() -> {
155-
Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), new JsonObject());
156-
})
177+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.UNSAFE, new JsonObject()))
157178
.isInstanceOf(BadRequestException.class)
158179
.hasMessageStartingWith("Unable to parse the Structurizr DSL. Error running inline script, caused by java.lang.RuntimeException: Could not load a scripting engine for extension \"kts\" at line 5");
159180
}
160181

182+
183+
@Test
184+
public void should_throw_exception_when_script_directive_used_safe() throws IOException {
185+
String source = read("./script.structurizr");
186+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, new JsonObject()))
187+
.isInstanceOf(BadRequestException.class)
188+
.hasMessageStartingWith("Unable to parse the Structurizr DSL. !script is not available when the parser is running in restricted mode at line 2");
189+
}
190+
161191
@Test
162192
public void should_throw_exception_when_unknown_output_specified() throws IOException {
163193
String source = read("./bigbank.structurizr");
164194

165195
JsonObject options = new JsonObject();
166196
options.put("output", "invalid");
167197

168-
assertThatThrownBy(() -> {
169-
Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), options);
170-
})
198+
assertThatThrownBy(() -> Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, options))
171199
.isInstanceOf(BadRequestException.class)
172200
.hasMessageStartingWith("Unknown output option: invalid");
173201
}
174202

175203
@Test
176204
public void should_preserve_styles_defined_in_workspace_while_applying_theme() throws IOException, InterruptedException {
177205
String source = read("./workspace-style-with-theme.structurizr");
178-
byte[] convert = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), new JsonObject());
206+
byte[] convert = Structurizr.convert(source, FileFormat.SVG, plantumlCommand, new StructurizrPlantUMLExporter(), SafeMode.SAFE, new JsonObject());
179207
assertThat(new String(convert)).isEqualTo(read("./workspace-style-with-theme.svg"));
180208
}
181209

@@ -184,7 +212,7 @@ private String stripComments(String xmlContent) {
184212
}
185213

186214
private String read(String name) throws IOException {
187-
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(StructurizrServiceTest.class.getClassLoader().getResourceAsStream(name)))) {
215+
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(Objects.requireNonNull(StructurizrServiceTest.class.getClassLoader().getResourceAsStream(name))))) {
188216
return buffer.lines().collect(Collectors.joining("\n"));
189217
}
190218
}

Diff for: server/src/test/resources/docs.expected.svg

+1
Loading

Diff for: server/src/test/resources/docs.structurizr

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
workspace {
2+
model {
3+
user = person "User"
4+
softwareSystem = softwareSystem "Software System" {
5+
!docs src/test/resources/docs
6+
}
7+
user -> softwareSystem "Uses"
8+
}
9+
10+
views {
11+
systemContext softwareSystem {
12+
include *
13+
autolayout
14+
}
15+
16+
theme default
17+
}
18+
}

Diff for: server/src/test/resources/docs/index.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# System documentation
2+
3+
4+
## Workspace
5+
6+
This is a *workspace*!

0 commit comments

Comments
 (0)