Initial T06 security template
This commit is contained in:
19
api/src/admin-service.js
Normal file
19
api/src/admin-service.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
|
||||
function startAdminService() {
|
||||
const app = express();
|
||||
|
||||
app.get('/admin', (req, res) => {
|
||||
res.json({
|
||||
service: 'internal-admin-service',
|
||||
warning: 'Ovaj servis ne bi smio biti dostupan kroz javni API.',
|
||||
secret: 'internal-admin-token-demo'
|
||||
});
|
||||
});
|
||||
|
||||
app.listen(3001, () => {
|
||||
console.log('Interni admin servis na http://localhost:3001');
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = startAdminService;
|
||||
37
api/src/auth.js
Normal file
37
api/src/auth.js
Normal file
@@ -0,0 +1,37 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||
|
||||
function privateKeyPath() {
|
||||
return path.join(__dirname, '..', 'private.pem');
|
||||
}
|
||||
|
||||
function publicKeyPath() {
|
||||
return path.join(__dirname, '..', 'public.pem');
|
||||
}
|
||||
|
||||
function signToken(user) {
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
username: user.username,
|
||||
role: user.role
|
||||
};
|
||||
|
||||
// TODO Z1.4:
|
||||
// Zamijeni simetrično HS256 potpisivanje asimetričnim RS256 potpisivanjem.
|
||||
// 1. Generiraj api/private.pem i api/public.pem.
|
||||
// 2. Umjesto JWT_SECRET koristi fs.readFileSync(privateKeyPath()).
|
||||
// 3. Promijeni algorithm iz HS256 u RS256.
|
||||
return jwt.sign(payload, JWT_SECRET, {
|
||||
algorithm: 'HS256',
|
||||
expiresIn: '15m'
|
||||
});
|
||||
}
|
||||
|
||||
function getPublicKeyPath() {
|
||||
return publicKeyPath();
|
||||
}
|
||||
|
||||
module.exports = { signToken, getPublicKeyPath, privateKeyPath, publicKeyPath };
|
||||
19
api/src/data.js
Normal file
19
api/src/data.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const users = [
|
||||
{ id: 'u0', username: 'student', password: 'fpmoz2024', name: 'Demo Student', role: 'student' },
|
||||
{ id: 'u1', username: 'student1', password: 'pass1', name: 'Student Jedan', role: 'student' },
|
||||
{ id: 'u2', username: 'student2', password: 'pass2', name: 'Student Dva', role: 'student' }
|
||||
];
|
||||
|
||||
const profiles = {
|
||||
u0: { id: 'u0', name: 'Demo Student', email: 'student@fpmoz.sum.ba', role: 'student' },
|
||||
u1: { id: 'u1', name: 'Student Jedan', email: 'student1@fpmoz.sum.ba', role: 'student' },
|
||||
u2: { id: 'u2', name: 'Student Dva', email: 'student2@fpmoz.sum.ba', role: 'student' }
|
||||
};
|
||||
|
||||
const orders = [
|
||||
{ id: 1, userId: 'u1', item: 'Knjiga: Mikroservisi', amount: 25 },
|
||||
{ id: 2, userId: 'u2', item: 'Knjiga: API Security', amount: 30 },
|
||||
{ id: 3, userId: 'u1', item: 'Tečaj: Docker osnove', amount: 15 }
|
||||
];
|
||||
|
||||
module.exports = { users, profiles, orders };
|
||||
26
api/src/middleware/authenticate.js
Normal file
26
api/src/middleware/authenticate.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const fs = require('fs');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getPublicKeyPath } = require('../auth');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-ne-koristiti-u-produkciji';
|
||||
|
||||
function authenticate(req, res, next) {
|
||||
const authHeader = req.headers.authorization || '';
|
||||
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Missing bearer token' });
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO Z1.4:
|
||||
// Nakon prelaska na RS256 ovdje umjesto JWT_SECRET koristi javni ključ:
|
||||
// jwt.verify(token, fs.readFileSync(getPublicKeyPath()))
|
||||
req.user = jwt.verify(token, JWT_SECRET);
|
||||
return next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = authenticate;
|
||||
18
api/src/routes/auth.js
Normal file
18
api/src/routes/auth.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const express = require('express');
|
||||
const { users } = require('../data');
|
||||
const { signToken } = require('../auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.post('/login', (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
const user = users.find((candidate) => candidate.username === username && candidate.password === password);
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ error: 'Invalid username or password' });
|
||||
}
|
||||
|
||||
return res.json({ token: signToken(user) });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
24
api/src/routes/orders.js
Normal file
24
api/src/routes/orders.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../middleware/authenticate');
|
||||
const { orders } = require('../data');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/orders/:id', authenticate, (req, res) => {
|
||||
const order = orders.find((candidate) => candidate.id === Number(req.params.id));
|
||||
|
||||
if (!order) {
|
||||
return res.status(404).json({ error: 'Order not found' });
|
||||
}
|
||||
|
||||
// TODO Z4.2:
|
||||
// Ova provjera je namjerno zakomentirana kako bi se demonstrirala BOLA ranjivost.
|
||||
// Odkomentiraj blok, restartaj server i ponovno testiraj pristup tuđoj narudžbi.
|
||||
// if (order.userId !== req.user.sub) {
|
||||
// return res.status(403).json({ error: 'Forbidden' });
|
||||
// }
|
||||
|
||||
return res.json(order);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
42
api/src/routes/preview.js
Normal file
42
api/src/routes/preview.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const express = require('express');
|
||||
const axios = require('axios');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/fetch-preview', async (req, res) => {
|
||||
const { url } = req.query;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({ error: 'Missing url query parameter' });
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO Z4.4:
|
||||
// Ova aplikacija je namjerno ranjiva na SSRF jer prihvaća bilo koji URL.
|
||||
// Odkomentiraj whitelist provjeru i dopusti samo poznate javne domene.
|
||||
// const ALLOWED_DOMAINS = ['example.com', 'httpbin.org', 'jsonplaceholder.typicode.com'];
|
||||
// const parsed = new URL(url);
|
||||
// if (!ALLOWED_DOMAINS.includes(parsed.hostname)) {
|
||||
// return res.status(403).json({
|
||||
// error: 'Domain not allowed',
|
||||
// allowed: ALLOWED_DOMAINS
|
||||
// });
|
||||
// }
|
||||
|
||||
const response = await axios.get(url, {
|
||||
timeout: 3000,
|
||||
maxRedirects: 2,
|
||||
validateStatus: () => true
|
||||
});
|
||||
|
||||
if (typeof response.data === 'object') {
|
||||
return res.status(response.status).json(response.data);
|
||||
}
|
||||
|
||||
return res.status(response.status).type('text/plain').send(String(response.data).slice(0, 2000));
|
||||
} catch (error) {
|
||||
return res.status(502).json({ error: 'Fetch failed', details: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
11
api/src/routes/profile.js
Normal file
11
api/src/routes/profile.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const express = require('express');
|
||||
const authenticate = require('../middleware/authenticate');
|
||||
const { profiles } = require('../data');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/profile', authenticate, (req, res) => {
|
||||
return res.json(profiles[req.user.sub] || { id: req.user.sub, message: 'Profil nije pronađen' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
59
api/src/server.js
Normal file
59
api/src/server.js
Normal file
@@ -0,0 +1,59 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const fs = require('fs');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const express = require('express');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const profileRoutes = require('./routes/profile');
|
||||
const ordersRoutes = require('./routes/orders');
|
||||
const previewRoutes = require('./routes/preview');
|
||||
const startAdminService = require('./admin-service');
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
app.get('/api/public', (req, res) => {
|
||||
res.json({ message: 'Javni endpoint radi.' });
|
||||
});
|
||||
|
||||
app.get('/api/internal', (req, res) => {
|
||||
const cert = req.socket.getPeerCertificate();
|
||||
|
||||
if (!req.client.authorized) {
|
||||
return res.status(401).json({ error: 'Client certificate required' });
|
||||
}
|
||||
|
||||
return res.json({ message: 'mTLS pristup odobren.', client: cert.subject });
|
||||
});
|
||||
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/api', profileRoutes);
|
||||
app.use('/api', ordersRoutes);
|
||||
app.use('/api', previewRoutes);
|
||||
|
||||
const useHttps = String(process.env.HTTPS || '').toLowerCase() === 'true';
|
||||
|
||||
if (useHttps) {
|
||||
const certsDir = path.join(process.cwd(), 'certs');
|
||||
const options = {
|
||||
key: fs.readFileSync(path.join(certsDir, 'server.key')),
|
||||
cert: fs.readFileSync(path.join(certsDir, 'server.crt')),
|
||||
ca: fs.readFileSync(path.join(certsDir, 'ca.crt')),
|
||||
requestCert: true,
|
||||
rejectUnauthorized: false
|
||||
};
|
||||
|
||||
https.createServer(options, app).listen(3443, () => {
|
||||
console.log('API na https://localhost:3443');
|
||||
});
|
||||
} else {
|
||||
http.createServer(app).listen(3000, () => {
|
||||
console.log('API na http://localhost:3000');
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.START_ADMIN_SERVICE !== 'false') {
|
||||
startAdminService();
|
||||
}
|
||||
Reference in New Issue
Block a user