apply security audit

This commit is contained in:
Matthias Hochmeister
2026-03-11 13:18:10 +01:00
parent e9463c1c66
commit 93a87a7ae9
18 changed files with 272 additions and 38 deletions

View File

@@ -34,10 +34,51 @@ interface LoginFlowCredentials {
appPassword: string;
}
/**
* Validates that a URL is safe to use as an outbound service endpoint.
* Rejects non-http(s) protocols and private/loopback IP ranges to prevent SSRF.
*/
function isValidServiceUrl(raw: string): boolean {
let parsed: URL;
try {
parsed = new URL(raw);
} catch {
return false;
}
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
return false;
}
const hostname = parsed.hostname.toLowerCase();
// Reject plain loopback / localhost names
if (hostname === 'localhost' || hostname === '::1') {
return false;
}
// Reject numeric IPv4 private / loopback / link-local ranges
const ipv4Parts = hostname.split('.');
if (ipv4Parts.length === 4) {
const [a, b] = ipv4Parts.map(Number);
if (
a === 127 || // 127.0.0.0/8 loopback
a === 10 || // 10.0.0.0/8 private
(a === 172 && b >= 16 && b <= 31) || // 172.16.0.0/12 private
(a === 192 && b === 168) || // 192.168.0.0/16 private
(a === 169 && b === 254) // 169.254.0.0/16 link-local
) {
return false;
}
}
return true;
}
async function initiateLoginFlow(): Promise<LoginFlowResult> {
const baseUrl = environment.nextcloudUrl;
if (!baseUrl) {
throw new Error('NEXTCLOUD_URL is not configured');
if (!baseUrl || !isValidServiceUrl(baseUrl)) {
throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL');
}
try {
@@ -60,6 +101,9 @@ async function initiateLoginFlow(): Promise<LoginFlowResult> {
}
async function pollLoginFlow(pollEndpoint: string, pollToken: string): Promise<LoginFlowCredentials | null> {
if (!isValidServiceUrl(pollEndpoint)) {
throw new Error('pollEndpoint is not a valid service URL');
}
try {
const response = await axios.post(pollEndpoint, `token=${pollToken}`, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -85,8 +129,8 @@ async function pollLoginFlow(pollEndpoint: string, pollToken: string): Promise<L
async function getConversations(loginName: string, appPassword: string): Promise<ConversationsResult> {
const baseUrl = environment.nextcloudUrl;
if (!baseUrl) {
throw new Error('NEXTCLOUD_URL is not configured');
if (!baseUrl || !isValidServiceUrl(baseUrl)) {
throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL');
}
try {