mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 19:26:33 +00:00
Merge pull request #4 from th30d4y/copilot/fix-security-vulnerabilities
Remove hardcoded secrets from frontend and backend
This commit is contained in:
+76
-3
@@ -110,16 +110,89 @@ def check_docker_availability():
|
|||||||
|
|
||||||
# ✅ ENHANCED: Flask app configuration with your .env variables
|
# ✅ ENHANCED: Flask app configuration with your .env variables
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
class MissingSecretError(ValueError):
|
||||||
|
"""Raised when a required secret is not set in environment variables."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_required_secret(env_var: str, description: str) -> str:
|
||||||
|
"""
|
||||||
|
Get required secret from environment.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
env_var: Name of the environment variable
|
||||||
|
description: Human-readable description of the secret
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The secret value from the environment
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
MissingSecretError: If the environment variable is not set
|
||||||
|
"""
|
||||||
|
value = os.getenv(env_var)
|
||||||
|
if not value:
|
||||||
|
raise MissingSecretError(f"{description} ({env_var}) must be set in environment variables for security. Do not use default values for secrets.")
|
||||||
|
return value
|
||||||
|
|
||||||
|
def get_dev_fallback_secret(name: str) -> str:
|
||||||
|
"""
|
||||||
|
Generate a persistent random secret for development use only.
|
||||||
|
|
||||||
|
Stores the secret in a file in the system temp directory to persist across restarts.
|
||||||
|
Files are created with restrictive permissions (0600) to limit access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Unique identifier for this secret (used in filename)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A 64-character hex string (32 bytes of randomness)
|
||||||
|
|
||||||
|
Security Note:
|
||||||
|
These secrets are stored in temp files and should only be used for development.
|
||||||
|
In production, always set proper secrets via environment variables.
|
||||||
|
"""
|
||||||
|
import tempfile
|
||||||
|
import stat
|
||||||
|
secret_file = os.path.join(tempfile.gettempdir(), f'.openlearnx_dev_{name}')
|
||||||
|
try:
|
||||||
|
if os.path.exists(secret_file):
|
||||||
|
with open(secret_file, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
# Generate new secret and persist it
|
||||||
|
new_secret = os.urandom(32).hex()
|
||||||
|
try:
|
||||||
|
with open(secret_file, 'w') as f:
|
||||||
|
f.write(new_secret)
|
||||||
|
# Set restrictive permissions (owner read/write only)
|
||||||
|
os.chmod(secret_file, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
except Exception:
|
||||||
|
pass # If we can't persist, just return the generated secret
|
||||||
|
return new_secret
|
||||||
|
|
||||||
|
# Validate required secrets at startup
|
||||||
|
try:
|
||||||
|
_secret_key = get_required_secret('SECRET_KEY', 'Flask secret key')
|
||||||
|
_jwt_secret_key = get_required_secret('JWT_SECRET_KEY', 'JWT secret key')
|
||||||
|
_admin_token = get_required_secret('ADMIN_TOKEN', 'Admin authentication token')
|
||||||
|
except MissingSecretError as e:
|
||||||
|
print(f"⚠️ SECURITY WARNING: {e}")
|
||||||
|
print("⚠️ Using persistent development secrets. Set proper secrets in production!")
|
||||||
|
_secret_key = os.getenv('SECRET_KEY') or get_dev_fallback_secret('secret_key')
|
||||||
|
_jwt_secret_key = os.getenv('JWT_SECRET_KEY') or get_dev_fallback_secret('jwt_secret_key')
|
||||||
|
_admin_token = os.getenv('ADMIN_TOKEN') or get_dev_fallback_secret('admin_token')
|
||||||
|
|
||||||
app.config.update(
|
app.config.update(
|
||||||
SECRET_KEY=os.getenv('SECRET_KEY', 'your-super-secret-key-change-this-in-production-openlearnx-2024'),
|
SECRET_KEY=_secret_key,
|
||||||
MONGODB_URI=os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'),
|
MONGODB_URI=os.getenv('MONGODB_URI', 'mongodb://localhost:27017/'),
|
||||||
WEB3_PROVIDER_URL=os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'),
|
WEB3_PROVIDER_URL=os.getenv('WEB3_PROVIDER_URL', 'http://127.0.0.1:8545'),
|
||||||
CONTRACT_ADDRESS=os.getenv('CONTRACT_ADDRESS', '0x739f0aCef964f87Bc7974D972a811f8417d74B4C'),
|
CONTRACT_ADDRESS=os.getenv('CONTRACT_ADDRESS', '0x739f0aCef964f87Bc7974D972a811f8417d74B4C'),
|
||||||
DEPLOYER_PRIVATE_KEY=os.getenv('DEPLOYER_PRIVATE_KEY'),
|
DEPLOYER_PRIVATE_KEY=os.getenv('DEPLOYER_PRIVATE_KEY'),
|
||||||
MINTER_PRIVATE_KEY=os.getenv('MINTER_PRIVATE_KEY'),
|
MINTER_PRIVATE_KEY=os.getenv('MINTER_PRIVATE_KEY'),
|
||||||
ADMIN_TOKEN=os.getenv('ADMIN_TOKEN', 'admin-secret-key'),
|
ADMIN_TOKEN=_admin_token,
|
||||||
# ✅ JWT Configuration from your .env
|
# ✅ JWT Configuration from your .env
|
||||||
JWT_SECRET_KEY=os.getenv('JWT_SECRET_KEY', 'openlearnx-jwt-secret-key-change-in-production'),
|
JWT_SECRET_KEY=_jwt_secret_key,
|
||||||
JWT_ACCESS_TOKEN_EXPIRES=timedelta(hours=int(os.getenv('JWT_EXPIRATION_HOURS', 168))),
|
JWT_ACCESS_TOKEN_EXPIRES=timedelta(hours=int(os.getenv('JWT_EXPIRATION_HOURS', 168))),
|
||||||
# ✅ IPFS Configuration from your .env
|
# ✅ IPFS Configuration from your .env
|
||||||
IPFS_GATEWAY=os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001'),
|
IPFS_GATEWAY=os.getenv('IPFS_GATEWAY', 'https://ipfs.infura.io:5001'),
|
||||||
|
|||||||
@@ -29,15 +29,12 @@ def admin_required(f):
|
|||||||
return jsonify({"error": "Invalid authorization format"}), 401
|
return jsonify({"error": "Invalid authorization format"}), 401
|
||||||
|
|
||||||
token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
|
token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None
|
||||||
print(f"Extracted token: '{token}'")
|
|
||||||
|
|
||||||
# Check environment variable first, then fallback to default
|
# Check environment variable - no fallback for security
|
||||||
expected_token = os.getenv('ADMIN_TOKEN')
|
expected_token = os.getenv('ADMIN_TOKEN')
|
||||||
if not expected_token:
|
if not expected_token:
|
||||||
expected_token = 'admin-secret-key'
|
print("❌ ADMIN_TOKEN environment variable not set")
|
||||||
|
return jsonify({"error": "Server configuration error: ADMIN_TOKEN not configured"}), 500
|
||||||
print(f"Expected token: '{expected_token}'")
|
|
||||||
print(f"Environment ADMIN_TOKEN: '{os.getenv('ADMIN_TOKEN')}'")
|
|
||||||
|
|
||||||
# Strip any whitespace from both tokens
|
# Strip any whitespace from both tokens
|
||||||
if token and expected_token:
|
if token and expected_token:
|
||||||
|
|||||||
+27
-2
@@ -16,8 +16,33 @@ mongo_uri = os.getenv('MONGODB_URI', 'mongodb://localhost:27017/')
|
|||||||
client = MongoClient(mongo_uri)
|
client = MongoClient(mongo_uri)
|
||||||
db = client.openlearnx
|
db = client.openlearnx
|
||||||
|
|
||||||
# JWT secret
|
# JWT secret - must be set via environment variable
|
||||||
JWT_SECRET = os.getenv('JWT_SECRET', 'your-secret-key-here')
|
JWT_SECRET = os.getenv('JWT_SECRET')
|
||||||
|
if not JWT_SECRET:
|
||||||
|
import warnings
|
||||||
|
import tempfile
|
||||||
|
import stat
|
||||||
|
import secrets as secrets_module
|
||||||
|
warnings.warn("JWT_SECRET environment variable not set. Using persistent dev secret.", UserWarning)
|
||||||
|
|
||||||
|
def _generate_and_store_secret():
|
||||||
|
"""Generate a random secret and store it with restrictive permissions."""
|
||||||
|
return secrets_module.token_hex(32)
|
||||||
|
|
||||||
|
# Use persistent file-based secret for development to avoid invalidating tokens on restart
|
||||||
|
_secret_file = os.path.join(tempfile.gettempdir(), '.openlearnx_dev_jwt_secret_auth')
|
||||||
|
try:
|
||||||
|
if os.path.exists(_secret_file):
|
||||||
|
with open(_secret_file, 'r') as f:
|
||||||
|
JWT_SECRET = f.read().strip()
|
||||||
|
if not JWT_SECRET:
|
||||||
|
JWT_SECRET = _generate_and_store_secret()
|
||||||
|
with open(_secret_file, 'w') as f:
|
||||||
|
f.write(JWT_SECRET)
|
||||||
|
# Set restrictive permissions (owner read/write only)
|
||||||
|
os.chmod(_secret_file, stat.S_IRUSR | stat.S_IWUSR)
|
||||||
|
except Exception:
|
||||||
|
JWT_SECRET = _generate_and_store_secret()
|
||||||
|
|
||||||
@bp.route('/nonce', methods=['POST', 'OPTIONS'])
|
@bp.route('/nonce', methods=['POST', 'OPTIONS'])
|
||||||
def get_nonce():
|
def get_nonce():
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ export default function AdminLogin() {
|
|||||||
// Check if already authenticated
|
// Check if already authenticated
|
||||||
const checkExistingAuth = async () => {
|
const checkExistingAuth = async () => {
|
||||||
const token = localStorage.getItem('admin_token')
|
const token = localStorage.getItem('admin_token')
|
||||||
if (token === 'admin-secret-key') {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
// Verify token with API
|
// Verify token with API - no hardcoded secret check
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
})
|
})
|
||||||
|
|||||||
+32
-36
@@ -62,6 +62,26 @@ export default function AdminDashboard() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// Authentication logic
|
// Authentication logic
|
||||||
|
// Helper function to get admin token from localStorage
|
||||||
|
const getAdminToken = (): string | null => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return localStorage.getItem('admin_token')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get authorization headers
|
||||||
|
const getAuthHeaders = (): Record<string, string> => {
|
||||||
|
const token = getAdminToken()
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true)
|
setIsClient(true)
|
||||||
|
|
||||||
@@ -69,14 +89,14 @@ export default function AdminDashboard() {
|
|||||||
try {
|
try {
|
||||||
await new Promise(resolve => setTimeout(resolve, 500))
|
await new Promise(resolve => setTimeout(resolve, 500))
|
||||||
|
|
||||||
const token = localStorage.getItem('admin_token')
|
const token = getAdminToken()
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token === 'admin-secret-key') {
|
// Verify token with API - no hardcoded secret check
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
headers: { 'Authorization': `Bearer ${token}` }
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
@@ -90,10 +110,7 @@ export default function AdminDashboard() {
|
|||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
}
|
}
|
||||||
} catch (apiError) {
|
} catch (apiError) {
|
||||||
setIsAuthenticated(true)
|
// If API is unavailable, don't allow access without verification
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('admin_token')
|
localStorage.removeItem('admin_token')
|
||||||
router.push('/admin/login')
|
router.push('/admin/login')
|
||||||
}
|
}
|
||||||
@@ -114,10 +131,7 @@ export default function AdminDashboard() {
|
|||||||
const fetchCourses = async () => {
|
const fetchCourses = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -141,7 +155,7 @@ export default function AdminDashboard() {
|
|||||||
const fetchStats = async () => {
|
const fetchStats = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/dashboard', {
|
||||||
headers: { 'Authorization': 'Bearer admin-secret-key' }
|
headers: getAuthHeaders()
|
||||||
})
|
})
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
@@ -161,10 +175,7 @@ export default function AdminDashboard() {
|
|||||||
console.log('🔍 Fetching modules for course:', courseId) // Debug log
|
console.log('🔍 Fetching modules for course:', courseId) // Debug log
|
||||||
|
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, {
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('🔍 Modules response status:', response.status) // Debug log
|
console.log('🔍 Modules response status:', response.status) // Debug log
|
||||||
@@ -209,10 +220,7 @@ export default function AdminDashboard() {
|
|||||||
console.log('🔍 Fetching lessons for module:', moduleId) // Debug log
|
console.log('🔍 Fetching lessons for module:', moduleId) // Debug log
|
||||||
|
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
||||||
headers: {
|
headers: getAuthHeaders()
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('🔍 Lessons response status:', response.status) // Debug log
|
console.log('🔍 Lessons response status:', response.status) // Debug log
|
||||||
@@ -253,10 +261,7 @@ export default function AdminDashboard() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
const response = await fetch('http://127.0.0.1:5000/api/admin/courses', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer admin-secret-key'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -277,10 +282,7 @@ export default function AdminDashboard() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer admin-secret-key'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -301,7 +303,7 @@ export default function AdminDashboard() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: { 'Authorization': 'Bearer admin-secret-key' }
|
headers: getAuthHeaders()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -333,10 +335,7 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${selectedCourse?.id}/modules`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/courses/${selectedCourse?.id}/modules`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer admin-secret-key'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -358,10 +357,7 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
const response = await fetch(`http://127.0.0.1:5000/api/admin/modules/${moduleId}/lessons`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: getAuthHeaders(),
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Bearer admin-secret-key'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
body: JSON.stringify(formData)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -108,23 +108,7 @@ export default function CoursePage() {
|
|||||||
let modulesData = null
|
let modulesData = null
|
||||||
let modulesResponse = null
|
let modulesResponse = null
|
||||||
|
|
||||||
try {
|
// Use public endpoint for course page (not admin endpoint)
|
||||||
modulesResponse = await fetch(`http://127.0.0.1:5000/api/admin/courses/${courseId}/modules`, {
|
|
||||||
headers: {
|
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (modulesResponse.ok) {
|
|
||||||
modulesData = await modulesResponse.json()
|
|
||||||
console.log('✅ Modules loaded from admin endpoint:', modulesData)
|
|
||||||
}
|
|
||||||
} catch (adminError) {
|
|
||||||
console.log('⚠️ Admin endpoint failed, trying public endpoint')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!modulesData || !modulesResponse?.ok) {
|
|
||||||
try {
|
try {
|
||||||
modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, {
|
modulesResponse = await fetch(`http://127.0.0.1:5000/api/courses/${courseId}/modules`, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -137,8 +121,7 @@ export default function CoursePage() {
|
|||||||
console.log('✅ Modules loaded from public endpoint:', modulesData)
|
console.log('✅ Modules loaded from public endpoint:', modulesData)
|
||||||
}
|
}
|
||||||
} catch (publicError) {
|
} catch (publicError) {
|
||||||
console.error('❌ Both module endpoints failed')
|
console.error('❌ Module endpoint failed')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulesData) {
|
if (modulesData) {
|
||||||
@@ -185,20 +168,12 @@ export default function CoursePage() {
|
|||||||
try {
|
try {
|
||||||
console.log('🔍 Fetching lessons for module:', module.id)
|
console.log('🔍 Fetching lessons for module:', module.id)
|
||||||
|
|
||||||
let lessonsResponse = await fetch(`http://127.0.0.1:5000/api/admin/modules/${module.id}/lessons`, {
|
// Use public endpoint for course page (not admin endpoint)
|
||||||
headers: {
|
const lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, {
|
||||||
'Authorization': 'Bearer admin-secret-key',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!lessonsResponse.ok) {
|
|
||||||
lessonsResponse = await fetch(`http://127.0.0.1:5000/api/modules/${module.id}/lessons`, {
|
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
if (lessonsResponse.ok) {
|
if (lessonsResponse.ok) {
|
||||||
const lessonData = await lessonsResponse.json()
|
const lessonData = await lessonsResponse.json()
|
||||||
|
|||||||
Reference in New Issue
Block a user