Skip to content
Open
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
445 changes: 445 additions & 0 deletions cross-origin-csrf-poc.html

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions frontend/env.development.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Development Environment Configuration Example
# Copy this file to .env.development and update values as needed
# This file contains safe URLs for development to prevent SSRF vulnerabilities

REACT_APP_API_BASE_URL=http://localhost:3000
REACT_APP_MODEL_TRIAL_URL=http://localhost:3000/model_trial
REACT_APP_NODE_ENV=development
7 changes: 7 additions & 0 deletions frontend/env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Production Environment Configuration Example
# Copy this file to .env.production and update values as needed
# This file contains secure URLs for production to prevent SSRF vulnerabilities

REACT_APP_API_BASE_URL=https://api.aixblock.io
REACT_APP_MODEL_TRIAL_URL=https://api.aixblock.io/model_trial
REACT_APP_NODE_ENV=production
7 changes: 7 additions & 0 deletions frontend/env.staging.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Staging Environment Configuration Example
# Copy this file to .env.staging and update values as needed
# This file contains secure URLs for staging to prevent SSRF vulnerabilities

REACT_APP_API_BASE_URL=https://staging-api.aixblock.io
REACT_APP_MODEL_TRIAL_URL=https://staging-api.aixblock.io/model_trial
REACT_APP_NODE_ENV=staging
34 changes: 16 additions & 18 deletions frontend/src/components/ModelMarketplace/ModelDetail/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -587,26 +587,24 @@ const Component = ({ item, project, onBackClick, onCompleted, needConfirmResetCo
const [, setError] = React.useState<string | null>(null);
// Check demo and stats
useEffect(() => {
const backendURL = "https://127.0.0.1:9090";
const projectID = 1;
const controller = new AbortController();

const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project: projectID.toString(),
}),
signal: controller.signal,
};

let url = backendURL;
// Import environment configuration to prevent SSRF vulnerability
import('../../../config/environment').then(({ getSafeModelTrialUrl }) => {
const projectID = 1;
const controller = new AbortController();

const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project: projectID.toString(),
}),
signal: controller.signal,
};

while (url.endsWith("/")) {
url = url.substring(0, url.length - 2);
}
// Use safe, environment-based URL instead of hardcoded internal URL
const modelTrialUrl = getSafeModelTrialUrl();

fetch(backendURL + "/model_trial", requestOptions)
fetch(modelTrialUrl, requestOptions)
.then(r => r.json())
.then(r => {
if (controller.signal.aborted || !Object.hasOwn(r, "share_url")) {
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/config/environment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Environment configuration for different deployment stages
// This file replaces hardcoded internal URLs with secure, environment-based configuration
// to prevent SSRF (Server-Side Request Forgery) vulnerabilities

export const environment = {
development: {
apiBaseUrl: 'http://localhost:3000',
modelTrialUrl: 'http://localhost:3000/model_trial'
},
staging: {
apiBaseUrl: 'https://staging-api.aixblock.io',
modelTrialUrl: 'https://staging-api.aixblock.io/model_trial'
},
production: {
apiBaseUrl: 'https://api.aixblock.io',
modelTrialUrl: 'https://api.aixblock.io/model_trial'
}
};

// Get current environment
export const getCurrentEnvironment = () => {
if (process.env.NODE_ENV === 'production') {
return environment.production;
} else if (process.env.NODE_ENV === 'staging') {
return environment.staging;
} else {
return environment.development;
}
};

// Get API base URL safely
export const getApiBaseUrl = () => {
const env = getCurrentEnvironment();
return env.apiBaseUrl;
};

// Get model trial URL safely
export const getModelTrialUrl = () => {
const env = getCurrentEnvironment();
return env.modelTrialUrl;
};

// Fallback to production if environment is not set
export const getSafeModelTrialUrl = () => {
try {
return getModelTrialUrl();
} catch (error) {
console.warn('Environment not configured, using production fallback');
return environment.production.modelTrialUrl;
}
};
35 changes: 16 additions & 19 deletions frontend/src/pages/Project/Settings/ML/ModelDetail/Index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -491,27 +491,24 @@ const ModelDetail = () => {
const [previewUrl, setPreviewUrl] = React.useState<string | null>(null);
const [error, setError] = React.useState<string | null>(null);
useEffect(() => {
// Import environment configuration to prevent SSRF vulnerability
import('../../../../config/environment').then(({ getSafeModelTrialUrl }) => {
const projectID = 1;
const controller = new AbortController();

const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project: projectID.toString(),
}),
signal: controller.signal,
};

const backendURL = "https://127.0.0.1:9090";
const projectID = 1;
const controller = new AbortController();

const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
project: projectID.toString(),
}),
signal: controller.signal,
};

let url = backendURL;

while (url.endsWith("/")) {
url = url.substring(0, url.length - 2);
}
// Use safe, environment-based URL instead of hardcoded internal URL
const modelTrialUrl = getSafeModelTrialUrl();

fetch(backendURL + "/model_trial", requestOptions)
fetch(modelTrialUrl, requestOptions)
.then(r => r.json())
.then(r => {
if (controller.signal.aborted || !Object.hasOwn(r, "share_url")) {
Expand Down
127 changes: 127 additions & 0 deletions frontend/src/utils/urlValidation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// URL validation utilities to prevent SSRF (Server-Side Request Forgery) attacks
// This file provides comprehensive security measures to prevent internal network access

export class UrlValidator {
private static readonly ALLOWED_PROTOCOLS = ['http:', 'https:'];
private static readonly ALLOWED_DOMAINS = [
'aixblock.io',
'api.aixblock.io',
'staging-api.aixblock.io',
'localhost' // Only for development
];

private static readonly BLOCKED_IPS = [
'127.0.0.1',
'localhost',
'0.0.0.0',
'::1'
];

/**
* Validates if a URL is safe to use (prevents SSRF)
*/
static isValidUrl(url: string): boolean {
try {
const parsedUrl = new URL(url);

// Check protocol
if (!this.ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
console.warn(`Blocked URL with invalid protocol: ${parsedUrl.protocol}`);
return false;
}

// Check for blocked IPs
if (this.BLOCKED_IPS.includes(parsedUrl.hostname)) {
console.warn(`Blocked URL with blocked IP: ${parsedUrl.hostname}`);
return false;
}

// Check for localhost variations
if (parsedUrl.hostname.includes('localhost') ||
parsedUrl.hostname.includes('127.0.0.1')) {
console.warn(`Blocked URL with localhost variation: ${parsedUrl.hostname}`);
return false;
}

// Check for private IP ranges
if (this.isPrivateIP(parsedUrl.hostname)) {
console.warn(`Blocked URL with private IP: ${parsedUrl.hostname}`);
return false;
}

// Check domain whitelist (only in production)
if (process.env.NODE_ENV === 'production') {
if (!this.ALLOWED_DOMAINS.some(domain =>
parsedUrl.hostname.endsWith(domain))) {
console.warn(`Blocked URL with unauthorized domain: ${parsedUrl.hostname}`);
return false;
}
}

return true;
} catch (error) {
console.error('URL validation error:', error);
return false;
}
}

/**
* Checks if an IP address is in private ranges
*/
private static isPrivateIP(hostname: string): boolean {
// Simple check for common private IP patterns
const privatePatterns = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./
];

return privatePatterns.some(pattern => pattern.test(hostname));
}

/**
* Sanitizes a URL to ensure it's safe
*/
static sanitizeUrl(url: string): string {
if (this.isValidUrl(url)) {
return url;
}

// Return safe default if URL is invalid
console.warn(`URL sanitized from ${url} to safe default`);
return 'https://api.aixblock.io';
}

/**
* Validates and sanitizes model trial URL specifically
*/
static getSafeModelTrialUrl(): string {
// Import the environment configuration
try {
// Dynamic import to avoid circular dependencies
const { getSafeModelTrialUrl } = require('../config/environment');
const url = getSafeModelTrialUrl();

if (this.isValidUrl(url)) {
return url;
}
} catch (error) {
console.warn('Environment config not available, using safe default');
}

// Return safe production URL as fallback
return 'https://api.aixblock.io/model_trial';
}

/**
* Logs security events for monitoring
*/
static logSecurityEvent(event: string, details: any): void {
if (process.env.NODE_ENV === 'production') {
console.warn(`[SECURITY] ${event}:`, details);
// In production, you might want to send this to a security monitoring service
} else {
console.log(`[SECURITY] ${event}:`, details);
}
}
}