plantuml/src/net/sourceforge/plantuml/security/SFile.java

417 lines
11 KiB
Java

/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2023, Arnaud Roques
*
* Project Info: http://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* http://plantuml.com/patreon (only 1$ per month!)
* http://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PlantUML distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
* License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*
*/
package net.sourceforge.plantuml.security;
import net.sourceforge.plantuml.log.Logger;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import javax.swing.ImageIcon;
/**
* Secure replacement for java.io.File.
* <p>
* This class should be used instead of java.io.File. There are few exceptions
* (mainly in the Swing part and in the ANT task)
* <p>
* This class does some control access and in secure mode hide the real path of
* file, so that it cannot be printed to end users.
*
*/
public class SFile implements Comparable<SFile> {
public static String separator = File.separator;
public static String pathSeparator = File.pathSeparator;
public static char separatorChar = File.separatorChar;
private final File internal;
@Override
public String toString() {
if (SecurityUtils.getSecurityProfile() == SecurityProfile.UNSECURE)
try {
return internal.getCanonicalPath();
} catch (IOException e) {
return internal.getAbsolutePath();
}
return super.toString();
}
public SFile(String nameOrPath) {
this(new File(nameOrPath));
}
public SFile(String dirname, String name) {
this(new File(dirname, name));
}
public SFile(SFile basedir, String name) {
this(new File(basedir.internal, name));
}
public SFile(URI uri) {
this(new File(uri));
}
private SFile(File internal) {
this.internal = internal;
}
public static SFile fromFile(File internal) {
if (internal == null) {
return null;
}
return new SFile(internal);
}
public SFile file(String name) {
return new SFile(this, name);
}
public boolean exists() {
if (internal != null && isFileOk())
return internal.exists();
return false;
}
public SFile getCanonicalFile() throws IOException {
return new SFile(internal.getCanonicalFile());
}
public boolean isAbsolute() {
return internal != null && internal.isAbsolute();
}
public boolean isDirectory() {
return internal != null && internal.exists() && internal.isDirectory();
}
public String getName() {
return internal.getName();
}
public boolean isFile() {
return internal != null && internal.isFile();
}
public long lastModified() {
return internal.lastModified();
}
public int compareTo(SFile other) {
return this.internal.compareTo(other.internal);
}
public String getPath() {
return internal.getPath();
}
public long length() {
return internal.length();
}
public boolean canWrite() {
return internal.canWrite();
}
public void setWritable(boolean b) {
internal.setWritable(b);
}
public void delete() {
internal.delete();
}
public Collection<SFile> listFiles() {
final File[] tmp = internal.listFiles();
if (tmp == null)
return Collections.emptyList();
final List<SFile> result = new ArrayList<>(tmp.length);
for (File f : tmp) {
result.add(new SFile(f));
}
return Collections.unmodifiableCollection(result);
}
public String[] list() {
return internal.list();
}
public SFile getAbsoluteFile() {
return new SFile(internal.getAbsoluteFile());
}
public SFile getParentFile() {
return new SFile(internal.getParentFile());
}
@Override
public int hashCode() {
return internal.hashCode();
}
@Override
public boolean equals(Object obj) {
return internal.equals(((SFile) obj).internal);
}
public String getAbsolutePath() {
return internal.getAbsolutePath();
}
public String getPrintablePath() {
if (SecurityUtils.getSecurityProfile() == SecurityProfile.UNSECURE) {
try {
return internal.getCanonicalPath();
} catch (IOException e) {
Logger.error(e);
}
}
return "";
}
public boolean canRead() {
return internal.canRead();
}
public void deleteOnExit() {
internal.deleteOnExit();
}
public void mkdirs() {
internal.mkdirs();
}
public static SFile createTempFile(String prefix, String suffix) throws IOException {
return new SFile(File.createTempFile(prefix, suffix));
}
public URI toURI() {
return internal.toURI();
}
public boolean renameTo(SFile dest) {
return internal.renameTo(dest.internal);
}
/**
* Check SecurityProfile to see if this file can be open.
*/
private boolean isFileOk() {
if (SecurityUtils.getSecurityProfile() == SecurityProfile.SANDBOX)
// In SANDBOX, we cannot read any files
return false;
// In any case SFile should not access the security folders
// (the files must be handled internally)
try {
if (isDenied())
return false;
} catch (IOException e) {
return false;
}
// Files in "plantuml.include.path" and "plantuml.allowlist.path" are ok.
if (isInAllowList(SecurityUtils.getPath(SecurityUtils.PATHS_INCLUDES)))
return true;
if (isInAllowList(SecurityUtils.getPath(SecurityUtils.ALLOWLIST_LOCAL_PATHS)))
return true;
if (SecurityUtils.getSecurityProfile() == SecurityProfile.INTERNET)
return false;
if (SecurityUtils.getSecurityProfile() == SecurityProfile.ALLOWLIST)
return false;
if (SecurityUtils.getSecurityProfile() != SecurityProfile.UNSECURE) {
// For UNSECURE, we did not do those checks
final String path = getCleanPathSecure();
if (path.startsWith("/etc/") || path.startsWith("/dev/") || path.startsWith("/boot/")
|| path.startsWith("/proc/") || path.startsWith("/sys/"))
return false;
if (path.startsWith("//"))
return false;
}
return true;
}
private boolean isInAllowList(List<SFile> allowlist) {
final String path = getCleanPathSecure();
for (SFile allow : allowlist) {
if (path.startsWith(allow.getCleanPathSecure())) {
// File directory is in the allowlist
return true;
}
}
return false;
}
/**
* Checks, if the SFile is inside the folder (-structure) of the security area.
*
* @return true, if the file is not allowed to read/write
* @throws IOException If an I/O error occurs, which is possible because the
* check the pathname may require filesystem queries
*/
private boolean isDenied() throws IOException {
SFile securityPath = SecurityUtils.getSecurityPath();
if (securityPath == null)
return false;
return getSanitizedPath().startsWith(securityPath.getSanitizedPath());
}
/**
* Returns a sanitized, canonical and normalized Path to a file.
*
* @return the Path
* @throws IOException If an I/O error occurs, which is possible because the
* construction of the canonical pathname may require
* filesystem queries
* @see #getCleanPathSecure()
* @see File#getCanonicalPath()
* @see Path#normalize()
*/
private Path getSanitizedPath() throws IOException {
return Paths.get(new File(getCleanPathSecure()).getCanonicalPath()).normalize();
}
private String getCleanPathSecure() {
String result = internal.getAbsolutePath();
result = result.replace("\0", "");
result = result.replace("\\\\", "/");
return result;
}
// Reading
// http://forum.plantuml.net/9048/img-tag-for-sequence-diagram-participants-does-always-render
public BufferedImage readRasterImageFromFile() {
// https://www.experts-exchange.com/questions/26171948/Why-are-ImageIO-read-images-losing-their-transparency.html
// https://stackoverflow.com/questions/18743790/can-java-load-images-with-transparency
if (isFileOk())
try {
return SecurityUtils.readRasterImage(new ImageIcon(this.getAbsolutePath()));
} catch (Exception e) {
Logger.error(e);
}
return null;
}
public BufferedReader openBufferedReader() {
if (isFileOk()) {
try {
return new BufferedReader(new FileReader(internal));
} catch (FileNotFoundException e) {
Logger.error(e);
}
}
return null;
}
public File conv() {
return internal;
}
public InputStream openFile() {
if (isFileOk())
try {
return new BufferedInputStream(new FileInputStream(internal));
} catch (FileNotFoundException e) {
Logger.error(e);
}
return null;
}
// Writing
public BufferedOutputStream createBufferedOutputStream() throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(internal));
}
public PrintWriter createPrintWriter() throws FileNotFoundException {
return new PrintWriter(internal);
}
public PrintWriter createPrintWriter(String charset) throws FileNotFoundException, UnsupportedEncodingException {
return new PrintWriter(internal, charset);
}
public FileOutputStream createFileOutputStream() throws FileNotFoundException {
return new FileOutputStream(internal);
}
public FileOutputStream createFileOutputStream(boolean append) throws FileNotFoundException {
return new FileOutputStream(internal, append);
}
public PrintStream createPrintStream() throws FileNotFoundException {
return new PrintStream(internal);
}
public PrintStream createPrintStream(String charset) throws FileNotFoundException, UnsupportedEncodingException {
return new PrintStream(internal, charset);
}
public PrintStream createPrintStream(Charset charset) throws FileNotFoundException, UnsupportedEncodingException {
return new PrintStream(internal, charset.name());
}
}