fix: login page warnings and implement secure URL testing
Fix browser console warnings on login page: - Make COOP header conditional on development mode (suppress HTTP warnings) - Add autocomplete attributes to 11 email/password inputs across 5 pages Implement server-side URL testing with enterprise-grade SSRF protection: - Replace window.open() with API-based connectivity check - Block private IPs (RFC 1918, loopback, link-local, ULA, IPv6 ranges) - DNS validation with 3s timeout before HTTP request - Block AWS metadata endpoint (169.254.169.254) - Block GCP metadata endpoint (metadata.google.internal) - HTTP HEAD request with 5s timeout - Maximum 2 redirects - Admin-only access enforcement Technical Implementation: - Backend: url_testing.go utility with isPrivateIP validation - Handler: TestPublicURL in settings_handler.go - Route: POST /settings/test-url (authenticated, admin-only) - Frontend: testPublicURL API call in settings.ts - UI: testPublicURLHandler in SystemSettings.tsx with toast feedback Test Coverage: - Backend: 85.8% (72 SSRF protection test cases passing) - Frontend: 86.85% (1,140 tests passing) - Security scans: Clean (Trivy, Go vuln check) - TypeScript: 0 type errors Closes: [issue number if applicable]
This commit is contained in:
196
docs/api.md
196
docs/api.md
@@ -469,6 +469,202 @@ preview_invite('admin@example.com')
|
||||
|
||||
---
|
||||
|
||||
#### Test URL Connectivity
|
||||
|
||||
Test if a URL is reachable from the server with comprehensive SSRF (Server-Side Request Forgery) protection.
|
||||
|
||||
```http
|
||||
POST /settings/test-url
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <admin-token>
|
||||
```
|
||||
|
||||
**Request Body:**
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://api.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
|
||||
- `url` (string) - The URL to test for connectivity
|
||||
|
||||
**Response 200 (Reachable):**
|
||||
|
||||
```json
|
||||
{
|
||||
"reachable": true,
|
||||
"latency": 145,
|
||||
"message": "URL is reachable",
|
||||
"error": ""
|
||||
}
|
||||
```
|
||||
|
||||
**Response 200 (Unreachable):**
|
||||
|
||||
```json
|
||||
{
|
||||
"reachable": false,
|
||||
"latency": 0,
|
||||
"message": "",
|
||||
"error": "connection timeout after 5s"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 400 (Invalid URL):**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid URL format"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 403 (Security Block):**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "URL resolves to a private IP address (blocked for security)",
|
||||
"details": "SSRF protection: private IP ranges are not allowed"
|
||||
}
|
||||
```
|
||||
|
||||
**Response 403 (Admin Required):**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Admin access required"
|
||||
}
|
||||
```
|
||||
|
||||
**Field Descriptions:**
|
||||
|
||||
- `reachable` - Boolean indicating if the URL is accessible
|
||||
- `latency` - Response time in milliseconds (0 if unreachable)
|
||||
- `message` - Success message describing the result
|
||||
- `error` - Error message if the test failed (empty on success)
|
||||
|
||||
**Security Features:**
|
||||
|
||||
This endpoint implements comprehensive SSRF protection:
|
||||
|
||||
1. **DNS Resolution Validation** - Resolves hostname with 3-second timeout
|
||||
2. **Private IP Blocking** - Blocks 13+ CIDR ranges:
|
||||
- RFC 1918 private networks (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)
|
||||
- Loopback addresses (`127.0.0.0/8`, `::1/128`)
|
||||
- Link-local addresses (`169.254.0.0/16`, `fe80::/10`)
|
||||
- IPv6 Unique Local Addresses (`fc00::/7`)
|
||||
- Multicast and other reserved ranges
|
||||
3. **Cloud Metadata Protection** - Blocks AWS (`169.254.169.254`) and GCP (`metadata.google.internal`) metadata endpoints
|
||||
4. **Controlled HTTP Request** - HEAD request with 5-second timeout
|
||||
5. **Limited Redirects** - Maximum 2 redirects allowed
|
||||
6. **Admin-Only Access** - Requires authenticated admin user
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
1. **Webhook validation:** Verify webhook endpoints before saving
|
||||
2. **Application URL testing:** Confirm configured URLs are reachable
|
||||
3. **Integration setup:** Test external service connectivity
|
||||
4. **Health checks:** Verify upstream service availability
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Test a public URL
|
||||
curl -X POST http://localhost:8080/api/v1/settings/test-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <admin-token>" \
|
||||
-d '{"url": "https://api.github.com"}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"reachable": true,
|
||||
"latency": 152,
|
||||
"message": "URL is reachable",
|
||||
"error": ""
|
||||
}
|
||||
|
||||
# Attempt to test a private IP (blocked)
|
||||
curl -X POST http://localhost:8080/api/v1/settings/test-url \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <admin-token>" \
|
||||
-d '{"url": "http://192.168.1.1"}'
|
||||
|
||||
# Response:
|
||||
{
|
||||
"error": "URL resolves to a private IP address (blocked for security)",
|
||||
"details": "SSRF protection: private IP ranges are not allowed"
|
||||
}
|
||||
```
|
||||
|
||||
**JavaScript Example:**
|
||||
|
||||
```javascript
|
||||
const testURL = async (url) => {
|
||||
const response = await fetch('http://localhost:8080/api/v1/settings/test-url', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer <admin-token>'
|
||||
},
|
||||
body: JSON.stringify({ url })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.reachable) {
|
||||
console.log(`✓ ${url} is reachable (${data.latency}ms)`);
|
||||
} else {
|
||||
console.error(`✗ ${url} failed: ${data.error}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
testURL('https://api.example.com');
|
||||
```
|
||||
|
||||
**Python Example:**
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
def test_url(url, api_base='http://localhost:8080/api/v1'):
|
||||
response = requests.post(
|
||||
f'{api_base}/settings/test-url',
|
||||
headers={
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer <admin-token>'
|
||||
},
|
||||
json={'url': url}
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
|
||||
if response.status_code == 403:
|
||||
print(f"Security block: {data.get('error')}")
|
||||
elif data.get('reachable'):
|
||||
print(f"✓ {url} is reachable ({data['latency']}ms)")
|
||||
else:
|
||||
print(f"✗ {url} failed: {data['error']}")
|
||||
|
||||
return data
|
||||
|
||||
test_url('https://api.github.com')
|
||||
```
|
||||
|
||||
**Security Considerations:**
|
||||
|
||||
- Only admin users can access this endpoint
|
||||
- Private IPs and cloud metadata endpoints are always blocked
|
||||
- DNS rebinding attacks are prevented by resolving before the HTTP request
|
||||
- Request timeouts prevent slowloris-style attacks
|
||||
- Limited redirects prevent redirect loops and excessive resource consumption
|
||||
- Consider rate limiting this endpoint in production environments
|
||||
|
||||
---
|
||||
|
||||
### SSL Certificates
|
||||
|
||||
#### List All Certificates
|
||||
|
||||
Reference in New Issue
Block a user