Skip to content
Closed
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
52 changes: 0 additions & 52 deletions .github/workflows/bandit.yml

This file was deleted.

1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: CI

permissions:
contents: read

on:
push:
pull_request:
Expand Down
61 changes: 0 additions & 61 deletions .github/workflows/codacy.yml

This file was deleted.

16 changes: 0 additions & 16 deletions .github/workflows/greetings.yml

This file was deleted.

22 changes: 0 additions & 22 deletions .github/workflows/label.yml

This file was deleted.

27 changes: 0 additions & 27 deletions .github/workflows/stale.yml

This file was deleted.

34 changes: 0 additions & 34 deletions .github/workflows/summary.yml

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ on:

jobs:
sync:
runs-on: ubuntu-latest
permissions:
contents: read
runs-on: ubuntu-latest

steps:
- name: Checkout repo
Expand Down
16 changes: 16 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,19 @@
**Prevention:**
1. Parse URLs and check hostnames against `localhost` and private IP ranges using `ipaddress` module.
2. Enforce strict length limits on user inputs (e.g., profile IDs) to prevent resource exhaustion or buffer abuse.

## 2025-01-24 - [SSRF via DNS Resolution]

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when references to undefined definitions are found.

[no-undefined-references] Found reference to undefined definition

Check notice

Code scanning / Remark-lint (reported by Codacy)

Warn when shortcut reference links are used.

[no-shortcut-reference-link] Use the trailing `[]` on reference links
**Vulnerability:** The `validate_folder_url` function blocked private IP literals but failed to resolve domain names to check their underlying IPs. An attacker could use a domain (e.g., `local.test`) that resolves to a private IP (e.g., `127.0.0.1`) to bypass the SSRF protection and access internal services.
**Learning:** Blocking IP literals is insufficient for SSRF protection. DNS resolution must be performed to verify that a domain does not resolve to a restricted IP address. Additionally, comprehensive checks are needed for all dangerous IP ranges (private, loopback, link-local, reserved, multicast).
**Prevention:**
1. Resolve domain names using `socket.getaddrinfo` before making requests.
2. Verify all resolved IP addresses against private/loopback/link-local/reserved/multicast ranges.
3. Handle DNS resolution failures securely (fail-closed), catching both `socket.gaierror` and `OSError`.
4. Strip IPv6 zone identifiers before IP validation.
**Known Limitations:**
- DNS rebinding attacks (TOCTOU): An attacker's DNS server could return a safe IP during validation and a private IP during the actual request. This is a fundamental limitation of DNS-based SSRF protection.
**Learning:** Blocking IP literals is insufficient for SSRF protection. DNS resolution must be performed to verify that a domain does not resolve to a restricted IP address. Note that check-time validation has TOCTOU limitations vs request-time verification.
**Prevention:**
1. Resolve domain names using `socket.getaddrinfo` before making requests.
2. Verify all resolved IP addresses against private, loopback, link-local, reserved, and multicast ranges.
3. Handle DNS resolution failures and zone identifiers (IPv6) securely.
21 changes: 0 additions & 21 deletions SECURITY.md

This file was deleted.

32 changes: 28 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import sys
import time
import re
import socket
import concurrent.futures
import threading
import ipaddress
Expand Down Expand Up @@ -205,12 +206,35 @@ def validate_folder_url(url: str) -> bool:

try:
ip = ipaddress.ip_address(hostname)
if ip.is_private or ip.is_loopback:
log.warning(f"Skipping unsafe URL (private IP): {sanitize_for_log(url)}")
if (ip.is_private or ip.is_loopback or
ip.is_link_local or ip.is_reserved or
ip.is_multicast):
log.warning(f"Skipping unsafe URL (restricted IP): {sanitize_for_log(url)}")
return False
except ValueError:
# Not an IP literal, it's a domain.
pass
# Not an IP literal, it's a domain. Resolve to check for private IPs.
# Note: This check has a Time-Of-Check-Time-Of-Use (TOCTOU) limitation.
# DNS rebinding attacks could return a safe IP during validation and a
# private IP during the actual request. This is a known limitation of
# DNS-based SSRF protection.
try:
# Resolve hostname to IPs
# Note: This check has a Time-Of-Check-Time-Of-Use (TOCTOU) limitation.
# A malicious DNS server could return a safe IP now and a private IP later.
addr_infos = socket.getaddrinfo(hostname, None)
for family, kind, proto, canonname, sockaddr in addr_infos:

Check warning

Code scanning / Prospector (reported by Codacy)

Unused variable 'family' (unused-variable)

Unused variable 'family' (unused-variable)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'canonname'

Unused variable 'canonname'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'canonname'

Unused variable 'canonname'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'kind'

Unused variable 'kind'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'proto'

Unused variable 'proto'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'proto'

Unused variable 'proto'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'kind'

Unused variable 'kind'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'family'

Unused variable 'family'

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'family'

Unused variable 'family'
ip_str = sockaddr[0]
# Strip IPv6 zone identifier (e.g., "%eth0") if present
ip_no_zone = ip_str.split('%', 1)[0]
resolved_ip = ipaddress.ip_address(ip_no_zone)
if (resolved_ip.is_private or resolved_ip.is_loopback or
resolved_ip.is_link_local or resolved_ip.is_reserved or
resolved_ip.is_multicast):
log.warning(f"Skipping unsafe URL (domain {sanitize_for_log(hostname)} resolves to restricted IP {resolved_ip}): {sanitize_for_log(url)}")
return False
except (socket.gaierror, OSError):
log.warning(f"Could not resolve hostname {sanitize_for_log(hostname)}, treating as unsafe.")

Check warning

Code scanning / Prospector (reported by Codacy)

Use lazy % formatting in logging functions (logging-fstring-interpolation)

Use lazy % formatting in logging functions (logging-fstring-interpolation)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (108/100)

Line too long (108/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (108/100)

Line too long (108/100)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Use lazy % formatting in logging functions

Use lazy % formatting in logging functions
return False

except Exception as e:
log.warning(f"Failed to validate URL {sanitize_for_log(url)}: {e}")
Expand Down
Loading