Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Gary Gregory">ArchiveStreamFactory now throws ArchiveException instead of IllegalArgumetException/IllegalStateException.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">ExactMath now throws ArchiveException instead of IllegalArgumetException/IllegalStateException.</action>
<!-- ADD -->
<action type="add" issue="COMPRESS-727" due-to="Tomas Illuminati">Add a security-first Extractor (org.apache.commons.compress.archivers.extractor) that safely extracts archives into a directory, including symbolic links, resisting symlink-slip and, on platforms with java.nio.file.SecureDirectoryStream, concurrent symlink races.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add MemoryLimitException.MemoryLimitException(long, long).</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add CompressException.CompressException(String, Object...).</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add ArchiveException.ArchiveException(String, Object...).</action>
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.archivers.extractor;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.SecureDirectoryStream;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Set;

import org.apache.commons.io.IOUtils;

/**
* Race-safe {@link Extractor} for platforms whose file system provider exposes a {@link SecureDirectoryStream}. The parent of
* each regular file is re-walked component by component from the root directory handle at write time, every component opened
* with {@link LinkOption#NOFOLLOW_LINKS}, and the file is created relative to the innermost directory handle (file
* descriptor). A third party therefore cannot win a time-of-check-to-time-of-use race by swapping a parent component for a
* symbolic link between resolution and write: the write targets the pinned directory inode, not a re-resolved path.
* <p>
* Directory, symbolic-link, and hard-link creation remain path-based (the {@code java.nio.file} API exposes no
* descriptor-relative {@code mkdirat}/{@code symlinkat}/{@code linkat}); the component verification still uses no-follow
* semantics, but creation of those node types carries the documented residual TOCTOU window. Regular-file writes, the common
* and highest-volume case, are fully race-safe.
* </p>
* <p>
* Instances are created by {@link Extractor#newExtractor(Path)}; this type is an implementation detail and is not part of the
* public API surface.
* </p>
*/
final class SecureExtractor extends Extractor {

@SuppressWarnings("unchecked")
private static SecureDirectoryStream<Path> asSecure(final DirectoryStream<Path> stream) {
return (SecureDirectoryStream<Path>) stream;
}

SecureExtractor(final Path rootDirectory) {
super(rootDirectory);
}

@Override
void writeFile(final Path leaf, final InputStream content) throws IOException {
final Path relative = rootDirectory.relativize(leaf);
final int count = relative.getNameCount();
final Path fileName = relative.getFileName();
final Deque<SecureDirectoryStream<Path>> handles = new ArrayDeque<>();
try {
SecureDirectoryStream<Path> dir = asSecure(Files.newDirectoryStream(rootDirectory));
handles.push(dir);
for (int i = 0; i < count - 1; i++) {
dir = asSecure(dir.newDirectoryStream(relative.getName(i), LinkOption.NOFOLLOW_LINKS));
handles.push(dir);
}
runBeforeLeafWrite();
final Set<OpenOption> options = new HashSet<>();
options.add(StandardOpenOption.WRITE);
options.add(LinkOption.NOFOLLOW_LINKS);
if (isOverwrite()) {
options.add(StandardOpenOption.CREATE);
options.add(StandardOpenOption.TRUNCATE_EXISTING);
} else {
options.add(StandardOpenOption.CREATE_NEW);
}
try (SeekableByteChannel channel = dir.newByteChannel(fileName, options);
OutputStream out = Channels.newOutputStream(channel)) {
IOUtils.copy(content, out);
}
} finally {
while (!handles.isEmpty()) {
handles.pop().close();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.archivers.extractor;

/**
* Controls how special entries (block devices, character devices, FIFOs) in an archive are handled during extraction. Such
* entries are never materialized by the secure path; this policy only decides whether their presence is tolerated.
*
* @since 1.29.0
*/
public enum SpecialFilePolicy {

/**
* Silently ignore special entries. This is the default.
*/
SKIP,

/**
* Fail extraction if the archive contains a special entry.
*/
REJECT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.commons.compress.archivers.extractor;

/**
* Controls how symbolic-link entries in an archive are handled during extraction. For untrusted archives, {@link #REJECT}
* (the default) and {@link #SKIP} are the safe choices; {@link #ALLOW_WITHIN_ROOT} enforces only a best-effort lexical
* containment check, as described on that constant.
*
* @since 1.29.0
*/
public enum SymlinkPolicy {

/**
* Fail extraction if the archive contains any symbolic-link entry. This is the default and the safest option for
* untrusted archives.
*/
REJECT,

/**
* Silently ignore symbolic-link entries; nothing is created for them.
*/
SKIP,

/**
* Create a symbolic link only when its target lexically resolves inside the extraction root, and reject a link whose
* target escapes. This containment check is best-effort: it is evaluated lexically against the archive-declared target at
* creation time, so it does not prevent a target reached through another symbolic link, nor a chain of individually
* in-root links that escapes only once the operating system resolves them together. Extraction itself never follows these
* links (every path component is resolved with {@link java.nio.file.LinkOption#NOFOLLOW_LINKS}), so nothing is written
* outside the root while extracting; the residual risk is a created link that a later consumer follows. For untrusted
* archives prefer {@link #REJECT} (the default) or {@link #SKIP}.
*/
ALLOW_WITHIN_ROOT,

/**
* Create symbolic links verbatim, including links whose target escapes the extraction root. Only appropriate for fully
* trusted archives.
*/
ALLOW_ALL
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Safe, symlink-resistant extraction of archives into a directory.
* <p>
* {@link org.apache.commons.compress.archivers.extractor.Extractor#newExtractor(java.nio.file.Path)} returns the strongest
* implementation the platform supports: a race-safe extractor backed by {@link java.nio.file.SecureDirectoryStream} where the
* file system provides one (Linux), otherwise a best-effort extractor that still resists symlink-slip from the archive but
* cannot fully close a concurrent TOCTOU race. Every path component is resolved with {@link java.nio.file.LinkOption#NOFOLLOW_LINKS}
* and regular files are written {@code CREATE_NEW} so a planted or archived symlink is never followed.
* </p>
*
* @since 1.29.0
*/
package org.apache.commons.compress.archivers.extractor;
73 changes: 73 additions & 0 deletions src/site/xdoc/examples.xml
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,79 @@ try (InputStream fi = Files.newInputStream(Paths.get("my.tar.gz"));

</subsection>

<subsection name="Safe Extraction">
<p>The skeleton above trusts the archive: it follows symbolic
links and can be raced by a concurrent attacker. Starting with
Compress 1.29 the
<code>org.apache.commons.compress.archivers.extractor.Extractor</code>
class materializes a whole archive into a target directory
safely, resisting both symlink-slip from the archive and
concurrent symlink races.</p>

<p>Obtain one with <code>Extractor.newExtractor(Path)</code>,
which returns the strongest implementation the platform
supports: a race-safe extractor backed by
<code>java.nio.file.SecureDirectoryStream</code> where the file
system provides one (Linux), otherwise a best-effort extractor
that still resists symlink-slip from the archive but cannot
fully close a concurrent time-of-check-to-time-of-use race.</p>

<source><![CDATA[
Path targetDir = ...
try (ArchiveInputStream<?> i = ... create the stream for your format, use buffering...) {
Extractor.newExtractor(targetDir).extract(i);
}
]]></source>

<p>Defaults are conservative: symbolic-link entries are
rejected, special entries such as devices and FIFOs are
skipped, and existing files are not overwritten. The handling
of symbolic links is controlled by <code>SymlinkPolicy</code>:</p>

<table>
<thead>
<tr>
<th>Policy</th>
<th>Behavior</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>REJECT</code> (default)</td>
<td>Fail if the archive contains any symbolic-link entry. Safest for untrusted archives.</td>
</tr>
<tr>
<td><code>SKIP</code></td>
<td>Silently ignore symbolic-link entries.</td>
</tr>
<tr>
<td><code>ALLOW_WITHIN_ROOT</code></td>
<td>Create a symbolic link only when its target lexically resolves inside the root. Best-effort, see the caveat below.</td>
</tr>
<tr>
<td><code>ALLOW_ALL</code></td>
<td>Create symbolic links verbatim. Only for fully trusted archives.</td>
</tr>
</tbody>
</table>

<source><![CDATA[
Extractor.newExtractor(targetDir)
.setSymlinkPolicy(SymlinkPolicy.ALLOW_WITHIN_ROOT)
.extract(archiveInputStream);
]]></source>

<p><code>ALLOW_WITHIN_ROOT</code> is best-effort: the
containment test is lexical and evaluated at creation time, so
it cannot prevent a link reached through another link, nor a
chain of individually in-root links that escapes only once the
operating system resolves them together. Extraction itself
never follows these links, so nothing is written outside the
root while extracting; the residual risk is a created link that
a later consumer follows. For untrusted archives use
<code>REJECT</code> (the default) or <code>SKIP</code>.</p>
</subsection>

<subsection name="Common Archival Logic">
<p>Apart from 7z all formats that support writing provide a
subclass of <code>ArchiveOutputStream</code> that can be used
Expand Down
Loading