Skip to content

Commit d324ea1

Browse files
committed
fix: validate mailing list name to prevent path traversal (CM-1318)
Signed-off-by: Uroš Marolt <uros@marolt.me>
1 parent 9985ca8 commit d324ea1

2 files changed

Lines changed: 38 additions & 1 deletion

File tree

backend/src/api/integration/helpers/mailingListAuthenticate.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,26 @@ import IntegrationService from '../../../services/integrationService'
55
import PermissionChecker from '../../../services/user/permissionChecker'
66
import { validateOrThrow } from '../../../utils/validation'
77

8+
const MAX_LIST_NAME_LENGTH = 255
9+
10+
// `name` becomes a single filesystem path component for the mailing list
11+
// mirror (see mirror_service.py's list_mirror_dir), so it must not contain a
12+
// path separator or resolve to "." / ".." when used alone.
13+
const isSafeListName = (name: string): boolean =>
14+
name.length > 0 &&
15+
name.length <= MAX_LIST_NAME_LENGTH &&
16+
!name.includes('/') &&
17+
!name.includes('\0') &&
18+
name !== '.' &&
19+
name !== '..'
20+
821
const bodySchema = z.object({
922
lists: z
1023
.array(
1124
z.object({
12-
name: z.string().trim().min(1),
25+
name: z.string().trim().min(1).refine(isSafeListName, {
26+
message: 'Invalid mailing list name',
27+
}),
1328
sourceUrl: z.string().trim().min(1),
1429
}),
1530
)

services/apps/mailing_list_integration/src/crowdmail/services/mirror/mirror_service.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
NetworkError,
1515
RateLimitError,
1616
RemoteServerError,
17+
ValidationError,
1718
)
1819
from crowdmail.logger import logger
1920
from crowdmail.services.parse.noteren import get_email_from_git, git_run_command
@@ -75,8 +76,29 @@ async def _run_shell_command(cmd: list[str], cwd: str | None = None, timeout: fl
7576
return ""
7677

7778

79+
MAX_LIST_NAME_LENGTH = 255
80+
81+
82+
def _validate_list_name(list_name: str) -> None:
83+
"""Reject anything that could escape the mirror root as a path component.
84+
85+
`list_name` becomes a single os.path.join component (no separators
86+
allowed), so the only way it can resolve outside `mirror_dir` is by being
87+
exactly "." or "..". Everything else is a literal directory name.
88+
"""
89+
if (
90+
not list_name
91+
or len(list_name) > MAX_LIST_NAME_LENGTH
92+
or "/" in list_name
93+
or "\0" in list_name
94+
or list_name in (".", "..")
95+
):
96+
raise ValidationError(f"Invalid mailing list name: {list_name!r}")
97+
98+
7899
def list_mirror_dir(list_name: str, mirror_dir: str = LORE_MIRROR_DIR) -> str:
79100
"""Local directory where a lore list mirror lives."""
101+
_validate_list_name(list_name)
80102
return os.path.join(mirror_dir, list_name)
81103

82104

0 commit comments

Comments
 (0)