Initial T06 security template

This commit is contained in:
2026-05-12 06:49:39 +00:00
commit af0ff49f87
20 changed files with 529 additions and 0 deletions

19
api/src/admin-service.js Normal file
View 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
View 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
View 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 };

View 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
View 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
View 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
View 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
View 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
View 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();
}