Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -1151,6 +1151,9 @@ private boolean validateId(
}

private boolean isValidId(String id) {
if (isPathTraversalSegment(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
char c = id.charAt(i);
if (!isValidIdCharacter(c)) {
Expand All @@ -1160,6 +1163,15 @@ private boolean isValidId(String id) {
return true;
}

/**
* An id or version made up only of {@code .} or {@code ..} is a filesystem path-traversal segment. The dotted

@elharo elharo Jul 7, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is true. The problem is mapping straight into a path traversal segment, not that these are the names. If you can find evidence that these are not legal names, please post it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair point, I don't have evidence that . or .. are illegal names as such. the actual problem is the default local repository layout using artifactId/version verbatim as directory names, which turns these values into literal ./.. path segments. reworded the javadoc in both validators to say the rejection is due to that mapping, not the names themselves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Unix and probably other systems . and .. are legal filenames. Dealing with this is a classic Unix sysadmin interview question.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agreed. the reworded javadoc makes no claim the names are illegal, only that the default layout uses them verbatim as directory names so they resolve as the current/parent directory. the rejection is pinned entirely on that mapping.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names are illegal, see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html, section 4.13:

The special filename dot shall refer to the directory specified by its predecessor. The special filename dot-dot shall refer to the parent directory of its predecessor directory. As a special case, in the root directory, dot-dot may refer to the root directory itself.

You can't create a file named . or ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or did you meant reserved instead of illegal maybe ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The section you cite refers to pathname resolution, not to pathnames themselves. In practice on most (all?) Unix and Unix like systems it is possible to create a file named "." or ".." as unwise as that might be.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you give me a reproducible way to create a file or directory named . or .. ?

echo > .
zsh: is a directory: .

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In python, open() returns 21 (EISDIR) on both . and ...

* characters pass the allowed-character checks yet map straight to a directory component when the value is turned
* into a local repository path, so such values are rejected here.
*/
private static boolean isPathTraversalSegment(String id) {
return ".".equals(id) || "..".equals(id);
}

private boolean isValidIdCharacter(char c) {
return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.';
}
Expand Down Expand Up @@ -1193,6 +1205,9 @@ private boolean validateIdWithWildcards(
}

private boolean isValidIdWithWildCards(String id) {
if (isPathTraversalSegment(id)) {
return false;
}
for (int i = 0; i < id.length(); i++) {
char c = id.charAt(i);
if (!isValidIdWithWildCardCharacter(c)) {
Expand Down Expand Up @@ -1644,6 +1659,18 @@ private boolean validateVersion(
return false;
}

if (isPathTraversalSegment(string)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be a valid version but is '" + string + "'.",
tracker);
return false;
}

return validateBannedCharacters(
prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,43 @@ void testInvalidCoordinateIds() throws Exception {
result.getErrors().get(1));
}

@Test
void testCoordinateIdsWithPathTraversal() throws Exception {
SimpleProblemCollector result = validate("coordinate-ids-path-traversal-pom.xml");

assertTrue(
result.getErrors().stream()
.anyMatch(m -> m.contains("'artifactId'") && m.contains("does not match a valid id pattern")),
"artifactId '..' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m ->
m.contains("dependencies.dependency.version") && m.contains("must be a valid version")),
"dependency version '..' must be rejected: " + result.getErrors());
}

@Test
void testCoordinateIdsWithSingleDotPathTraversal() throws Exception {
SimpleProblemCollector result = validate("coordinate-ids-path-traversal-dot-pom.xml");

assertTrue(
result.getErrors().stream()
.anyMatch(m -> m.contains("'groupId'") && m.contains("does not match a valid id pattern")),
"groupId '..' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m -> m.contains("'artifactId'") && m.contains("does not match a valid id pattern")),
"artifactId '.' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m ->
m.contains("dependencies.dependency.version") && m.contains("must be a valid version")),
"dependency version '.' must be rejected: " + result.getErrors());
}

@Test
void testMissingType() throws Exception {
SimpleProblemCollector result = validate("missing-type-pom.xml");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
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

http://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.
-->

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>..</groupId>
<artifactId>.</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>other.group</groupId>
<artifactId>lib</artifactId>
<version>.</version>
<type>jar</type>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
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

http://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.
-->

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>test.group</groupId>
<artifactId>..</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>other.group</groupId>
<artifactId>lib</artifactId>
<version>..</version>
<type>jar</type>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -1790,6 +1790,9 @@ private boolean validateCoordinatesId(
}

private boolean isValidCoordinatesId(String id) {
if (isPathTraversalSegment(id)) {
return false;
}
for (int index = 0; index < id.length(); index++) {
char character = id.charAt(index);
if (!isValidCoordinatesIdCharacter(character)) {
Expand All @@ -1799,6 +1802,15 @@ private boolean isValidCoordinatesId(String id) {
return true;
}

/**
* A coordinate id or version made up only of {@code .} or {@code ..} is a filesystem path-traversal segment.
* The dotted characters pass the allowed-character checks yet map straight to a directory component when the
* value is turned into a local repository path, so such values are rejected here.
*/
private static boolean isPathTraversalSegment(String id) {
return ".".equals(id) || "..".equals(id);
}

private boolean isValidCoordinatesIdCharacter(char character) {
return character >= 'a' && character <= 'z'
|| character >= 'A' && character <= 'Z'
Expand Down Expand Up @@ -1879,6 +1891,9 @@ private boolean validateCoordinatesIdWithWildcards(
}

private boolean isValidCoordinatesIdWithWildCards(String id) {
if (isPathTraversalSegment(id)) {
return false;
}
for (int index = 0; index < id.length(); index++) {
char character = id.charAt(index);
if (!isValidCoordinatesIdWithWildCardCharacter(character)) {
Expand Down Expand Up @@ -2342,6 +2357,18 @@ private boolean validateVersion(
return false;
}

if (isPathTraversalSegment(string)) {
addViolation(
problems,
severity,
version,
prefix + fieldName,
sourceHint,
"must be a valid version but is '" + string + "'.",
tracker);
return false;
}

return validateBannedCharacters(
prefix, fieldName, problems, severity, version, string, sourceHint, tracker, ILLEGAL_VERSION_CHARS);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,46 @@ void testInvalidCoordinateIds() throws Exception {
result.getErrors().get(1));
}

@Test
void testCoordinateIdsWithPathTraversal() throws Exception {
SimpleProblemCollector result = validate("coordinate-ids-path-traversal-pom.xml");

assertTrue(
result.getErrors().stream()
.anyMatch(m -> m.contains("'artifactId'")
&& m.contains("does not match a valid coordinate id pattern")),
"artifactId '..' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m ->
m.contains("dependencies.dependency.version") && m.contains("must be a valid version")),
"dependency version '..' must be rejected: " + result.getErrors());
}

@Test
void testCoordinateIdsWithSingleDotPathTraversal() throws Exception {
SimpleProblemCollector result = validate("coordinate-ids-path-traversal-dot-pom.xml");

assertTrue(
result.getErrors().stream()
.anyMatch(m ->
m.contains("'groupId'") && m.contains("does not match a valid coordinate id pattern")),
"groupId '..' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m -> m.contains("'artifactId'")
&& m.contains("does not match a valid coordinate id pattern")),
"artifactId '.' must be rejected: " + result.getErrors());

assertTrue(
result.getErrors().stream()
.anyMatch(m ->
m.contains("dependencies.dependency.version") && m.contains("must be a valid version")),
"dependency version '.' must be rejected: " + result.getErrors());
}

@Test
void testMissingType() throws Exception {
SimpleProblemCollector result = validate("missing-type-pom.xml");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
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

http://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.
-->

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>..</groupId>
<artifactId>.</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>other.group</groupId>
<artifactId>lib</artifactId>
<version>.</version>
<type>jar</type>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!--
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

http://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.
-->

<project>
<modelVersion>4.0.0</modelVersion>
<groupId>test.group</groupId>
<artifactId>..</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>other.group</groupId>
<artifactId>lib</artifactId>
<version>..</version>
<type>jar</type>
</dependency>
</dependencies>
</project>
Loading