Skip to content

Commit e329cb4

Browse files
tresfakberenz
andauthored
System properties to harden file reading/writing, http behavior (#1213)
* Disable file writing by default * Restrict file reading * Allow hardening of HTTP options Per #1210, #730 --------- Co-authored-by: Berenz <[email protected]>
1 parent 3c0ab94 commit e329cb4

13 files changed

+119
-77
lines changed

js/qz-tray.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1074,14 +1074,6 @@ var qz = (function() {
10741074
if (typeof newPrinter === 'string') {
10751075
newPrinter = { name: newPrinter };
10761076
}
1077-
1078-
if(newPrinter && newPrinter.file) {
1079-
// TODO: Warn for UNC paths too https://github.com/qzind/tray/issues/730
1080-
if(newPrinter.file.indexOf("\\\\") != 0) {
1081-
_qz.log.warn("Printing to file is deprecated. See https://github.com/qzind/tray/issues/730");
1082-
}
1083-
}
1084-
10851077
this.printer = newPrinter;
10861078
};
10871079

sample.html

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ <h3 class="panel-title">Printer</h3>
121121
<div class="form-group">
122122
<div class="btn-group" role="group">
123123
<button type="button" class="btn btn-default btn-sm" onclick="setPrinter($('#printerSearch').val());">Set To Search</button>
124-
<button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#askFileModal">Set To File</button>
125124
<button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#askHostModal">Set To Host</button>
126125
</div>
127126
<button type="button" class="btn btn-warning btn-sm" onclick="clearQueue($('#printerSearch').val());">Clear Queue</button>
@@ -1152,7 +1151,7 @@ <h3>Printer Status</h3>
11521151
<div class="col-md-6 pull-right">
11531152
<div class="form-group form-inline">
11541153
<label class="tip" for="maxJobData" data-toggle="tooltip" title="Maximum size in bytes of spool file content to return (Windows only)">
1155-
Raw Job Data Size
1154+
Raw Job Data Size
11561155
</label>
11571156
<input type="number" id="maxJobData" class="pull-right"/>
11581157
</div>
@@ -1354,27 +1353,6 @@ <h4 class="panel-title">Options</h4>
13541353
</div>
13551354
</div>
13561355

1357-
1358-
<div class="modal fade" id="askFileModal" role="dialog">
1359-
<div class="modal-dialog modal-sm" role="document">
1360-
<div class="modal-content">
1361-
<div class="modal-body">
1362-
<div class="form-group">
1363-
<label for="askFile">File:</label>
1364-
<input type="text" id="askFile" class="form-control" value="C:\tmp\example-file.txt" />
1365-
<hr />
1366-
<p><span class="text-danger" style="font-weight:bold;"><span class="fa fa-warning"></span> WARNING:</span> This feature has been deprecated. Please configure a local raw <code>FILE:</code> printer, or use <code>File IO</code></a> instead. For more
1367-
information please see <a href="https://github.com/qzind/tray/issues/730">issue&nbsp;<code>#730</code>.</a></p>
1368-
</div>
1369-
</div>
1370-
<div class="modal-footer">
1371-
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
1372-
<button type="button" class="btn btn-primary" onclick="setPrintFile();">Set</button>
1373-
</div>
1374-
</div>
1375-
</div>
1376-
</div>
1377-
13781356
<div class="modal fade" id="askHostModal" role="dialog">
13791357
<div class="modal-dialog modal-sm" role="document">
13801358
<div class="modal-content">
@@ -2982,13 +2960,6 @@ <h4 class="panel-title">Options</h4>
29822960
}).catch(displayError);
29832961
}
29842962

2985-
$("#askFileModal").on("shown.bs.modal", function() {
2986-
$("#askFile").focus().select();
2987-
});
2988-
$("#askHostModal").on("shown.bs.modal", function() {
2989-
$("#askHost").focus().select();
2990-
});
2991-
29922963
//make dirty when changed
29932964
$("input").add("select").on('change', function() {
29942965
$(this).addClass("dirty");
@@ -3375,11 +3346,6 @@ <h4 class="panel-title">Options</h4>
33753346
return options;
33763347
}
33773348

3378-
function setPrintFile() {
3379-
setPrinter({ file: $("#askFile").val() });
3380-
$("#askFileModal").modal('hide');
3381-
}
3382-
33833349
function setPrintHost() {
33843350
setPrinter({ host: $("#askHost").val(), port: $("#askPort").val() });
33853351
$("#askHostModal").modal('hide');

src/qz/auth/CRL.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static CRL getInstance() {
3838
public void run() {
3939
log.info("Loading CRL from {}", CRL_URL);
4040

41-
try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(CRL_URL)))) {
41+
try(BufferedReader br = new BufferedReader(new InputStreamReader(ConnectionUtilities.getInputStream(CRL_URL, false)))) {
4242
String line;
4343
while((line = br.readLine()) != null) {
4444
//Ignore empty and commented lines

src/qz/common/TrayManager.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,10 @@ private void blackList(Certificate cert) {
534534
}
535535
}
536536

537-
public void setServer(Server server, int insecurePortIndex) {
537+
public void setServer(Server server, int insecurePortInUse, int securePortInUse) {
538538
if (server != null && server.getConnectors().length > 0) {
539-
singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortIndex);
539+
singleInstanceCheck(PrintSocketServer.INSECURE_PORTS, insecurePortInUse, false);
540+
singleInstanceCheck(PrintSocketServer.SECURE_PORTS, securePortInUse, true);
540541

541542
displayInfoMessage("Server started on port(s) " + PrintSocketServer.getPorts(server));
542543

@@ -632,10 +633,10 @@ private void displayMessage(final String caption, final String text, final TrayI
632633
}
633634
}
634635

635-
public void singleInstanceCheck(java.util.List<Integer> insecurePorts, Integer insecurePortIndex) {
636-
for(int port : insecurePorts) {
637-
if (port != insecurePorts.get(insecurePortIndex)) {
638-
new SingleInstanceChecker(this, port);
636+
public void singleInstanceCheck(java.util.List<Integer> ports, Integer portInUse, boolean usingSecure) {
637+
for(int port : ports) {
638+
if (portInUse == -1 || port != ports.get(portInUse)) {
639+
new SingleInstanceChecker(this, port, usingSecure);
639640
}
640641
}
641642
}

src/qz/installer/certificate/CertificateManager.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.apache.logging.log4j.Logger;
3030
import qz.common.Constants;
3131
import qz.installer.Installer;
32+
import qz.utils.ArgValue;
3233
import qz.utils.FileUtilities;
3334
import qz.utils.MacUtilities;
3435
import qz.utils.SystemUtilities;
@@ -60,10 +61,7 @@ public class CertificateManager {
6061

6162
public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12";
6263
public static String DEFAULT_KEYSTORE_EXTENSION = ".p12";
63-
6464
public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt";
65-
66-
private static String DEFAULT_HOST_SCOPE = "0.0.0.0";
6765
private static int DEFAULT_PASSWORD_BITS = 100;
6866

6967
private boolean needsInstall;
@@ -331,7 +329,7 @@ public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) thro
331329
props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias());
332330

333331
if (keyPair.getType() == SSL) {
334-
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), DEFAULT_HOST_SCOPE);
332+
props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal());
335333
}
336334

337335

src/qz/printer/action/PrintImage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce
8282
case PLAIN:
8383
// There's really no such thing as a 'PLAIN' image, assume it's a URL
8484
case FILE:
85-
bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data")));
85+
bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true));
8686
break;
8787
default:
8888
bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data"))));

src/qz/printer/action/PrintPDF.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public void parseData(JSONArray printData, PrintOptions options) throws JSONExce
118118
case PLAIN:
119119
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
120120
case FILE:
121-
doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data")));
121+
doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true));
122122
break;
123123
default:
124124
doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data"))));

src/qz/printer/action/PrintRaw.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import com.ibm.icu.text.ArabicShapingException;
1313
import org.apache.commons.lang3.StringUtils;
14-
import org.apache.commons.ssl.Base64;
1514
import org.apache.pdfbox.pdmodel.PDDocument;
1615
import org.apache.pdfbox.pdmodel.common.PDRectangle;
1716
import org.apache.pdfbox.rendering.PDFRenderer;
@@ -175,7 +174,7 @@ private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilit
175174
case PLAIN:
176175
// There's really no such thing as a 'PLAIN' image, assume it's a URL
177176
case FILE:
178-
bi = ImageIO.read(ConnectionUtilities.getInputStream(data));
177+
bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true));
179178
break;
180179
default:
181180
bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
@@ -191,7 +190,7 @@ private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilitie
191190
case PLAIN:
192191
// There's really no such thing as a 'PLAIN' PDF, assume it's a URL
193192
case FILE:
194-
doc = PDDocument.load(ConnectionUtilities.getInputStream(data));
193+
doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true));
195194
break;
196195
default:
197196
doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts)));
@@ -329,7 +328,7 @@ public void print(PrintOutput output, PrintOptions options) throws PrintExceptio
329328
if (output.isSetHost()) {
330329
printToHost(output.getHost(), output.getPort(), bab.getByteArray());
331330
} else if (output.isSetFile()) {
332-
printToFile(output.getFile(), bab.getByteArray());
331+
printToFile(output.getFile(), bab.getByteArray(), true);
333332
} else {
334333
if (rawOpts.isForceRaw()) {
335334
if(tempFiles == null) {
@@ -339,7 +338,7 @@ public void print(PrintOutput output, PrintOptions options) throws PrintExceptio
339338
if(tempFiles.size() <= j) {
340339
tempFile = File.createTempFile("qz_raw_", null);
341340
tempFiles.add(j, tempFile);
342-
printToFile(tempFile, bab.getByteArray());
341+
printToFile(tempFile, bab.getByteArray(), false);
343342
} else {
344343
tempFile = tempFiles.get(j);
345344
}
@@ -401,7 +400,15 @@ private void printToHost(String host, int port, byte[] cmds) throws IOException
401400
*
402401
* @param file File to be written
403402
*/
404-
private void printToFile(File file, byte[] cmds) throws IOException {
403+
private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException {
404+
if(file == null) throw new IOException("No file specified");
405+
406+
if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) {
407+
log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.",
408+
file, ArgValue.SECURITY_PRINT_TOFILE.getMatch());
409+
throw new IOException(String.format("Printing to file '%s' is not permitted", file));
410+
}
411+
405412
log.debug("Printing to file: {}", file.getName());
406413

407414
//throws any exception and auto-closes stream

src/qz/utils/ArgValue.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ public enum ArgValue {
6969
"security.file.enabled"),
7070
SECURITY_FILE_STRICT(PREFERENCES, "Enable/disable signing requirements for File Communications features", null, true,
7171
"security.file.strict"),
72+
SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https",
73+
"security.data.protocols"),
74+
SECURITY_PRINT_TOFILE(PREFERENCES, "Enable/disable printing directly to file paths", null, false,
75+
"security.print.tofile"),
76+
SECURITY_WSS_SNISTRICT(PREFERENCES, "Enables strict http/websocket SNI checks", null, false,
77+
"security.wss.snistrict"),
78+
SECURITY_WSS_HTTPSONLY(PREFERENCES, "Disables insecure http/websocket ports (e.g. '8182')", null, false,
79+
"security.wss.httpsonly"),
80+
SECURITY_WSS_HOST(PREFERENCES, "Influences which physical adapter to bind to by setting the host parameter for http/websocket listening", null, "0.0.0.0",
81+
"security.wss.host"),
7282
LOG_DISABLE(PREFERENCES, "Disable/enable logging features", null, false,
7383
"log.disable"),
7484
LOG_ROTATE(PREFERENCES, "Number of log files to retain when the size fills up", null, 5,

src/qz/utils/ConnectionUtilities.java

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,11 @@
1212
import java.awt.*;
1313
import java.io.IOException;
1414
import java.io.InputStream;
15+
import java.net.MalformedURLException;
16+
import java.net.URISyntaxException;
1517
import java.net.URL;
1618
import java.net.URLConnection;
19+
import java.nio.file.Paths;
1720
import java.security.cert.Certificate;
1821
import java.security.cert.X509Certificate;
1922
import java.util.HashMap;
@@ -39,9 +42,17 @@ public final class ConnectionUtilities {
3942
*
4043
* @param urlString an absolute URL giving location of resource to read.
4144
*/
42-
public static InputStream getInputStream(String urlString) throws IOException {
45+
public static InputStream getInputStream(String urlString, boolean protocolRestricted) throws IOException {
4346
try {
44-
URLConnection urlConn = new URL(urlString).openConnection();
47+
URL url = new URL(urlString);
48+
if(protocolRestricted) {
49+
String allowed = PrefsSearch.getString(ArgValue.SECURITY_DATA_PROTOCOLS);
50+
if(!isAllowed(allowed, url)) {
51+
log.error("URL '{}' is not a valid http or https location. Configure property '{}' to modify this behavior.", url, ArgValue.SECURITY_DATA_PROTOCOLS.getMatch());
52+
throw new IOException(String.format("URL '%s' is not a valid [%s] location", url, allowed));
53+
}
54+
}
55+
URLConnection urlConn = url.openConnection();
4556
for( String key : getRequestProperties().keySet()) {
4657
urlConn.setRequestProperty(key, requestProps.get(key));
4758
}
@@ -54,6 +65,34 @@ public static InputStream getInputStream(String urlString) throws IOException {
5465
}
5566
}
5667

68+
private static boolean isAllowed(String allowed, URL url) {
69+
if(url == null) return false;
70+
String urlProtocol = url.getProtocol();
71+
if(urlProtocol == null || urlProtocol.trim().isEmpty()) return false;
72+
allowed = ArgValue.SECURITY_DATA_PROTOCOLS.getDefaultVal() +
73+
(allowed == null || allowed.trim().isEmpty() ? "" : "," + allowed);
74+
String[] protocols = allowed.split(",");
75+
// Loop over http, https, etc
76+
for(String protocol : protocols) {
77+
if(urlProtocol.trim().equalsIgnoreCase(protocol.trim())) {
78+
return true;
79+
}
80+
}
81+
// Allow exception for file: demo/assets
82+
if(urlProtocol.trim().toLowerCase(Locale.ENGLISH).equals("file")) {
83+
try {
84+
// Sanitize manipulative URLs
85+
url = Paths.get(url.toURI()).normalize().toUri().toURL();
86+
if (url.getPath().matches(".*/demo/assets/.*|.*/tray/assets/.*")) {
87+
log.warn("Allowing printing from restricted protocol '{}:' for demo asset '{}'", urlProtocol, url);
88+
return true;
89+
}
90+
}
91+
catch(URISyntaxException | MalformedURLException ignore) {}
92+
}
93+
return false;
94+
}
95+
5796
/**
5897
* A blind SSL trust manager, for debugging SSL issues
5998
*/

src/qz/utils/FileUtilities.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -592,7 +592,7 @@ public static String readLocalFile(Path path) throws IOException {
592592
}
593593

594594
public static byte[] readRawFile(String url) throws IOException {
595-
return readFile(new DataInputStream(ConnectionUtilities.getInputStream(url)));
595+
return readFile(new DataInputStream(ConnectionUtilities.getInputStream(url, true)));
596596
}
597597

598598
private static byte[] readFile(DataInputStream in) throws IOException {

0 commit comments

Comments
 (0)