Skip to content
3 changes: 2 additions & 1 deletion src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ The <action> type attribute can be add,update,fix,remove.
</properties>
<body>
<release version="1.29.0" date="YYYY-MM-DD" description="This is a feature and maintenance release. Java 8 or later is required. This release updates Apache Commons Lang to 3.18.0 to pick up the fix for CVE-2025-48924 (https://nvd.nist.gov/vuln/detail/CVE-2025-48924), but is not affected by it.">
<!-- FIX sevenz -->
<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>
<!-- FIX sevenz -->
<action type="fix" issue="COMPRESS-702" dev="ggregory" due-to="Zhang Di, Lastoneee">Performance issue in SevenZFile #681.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix kilobyte to kibibyte conversion in SevenZFile.ArchiveStatistics.assertValidity(int).</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix kilobyte to kibibyte conversion in SevenZFile.ArchiveStatistics.toString().</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