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
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "msedge",
"request": "launch",
"name": "Open redovisning-demo.html",
"file": "c:\\Users\\mario\\Desktop\\projekt\\yh-message-app-fullstack\\redovisning-demo.html"
}
]
}
15 changes: 15 additions & 0 deletions backend/middleware/auth.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
import jwt from "jsonwebtoken"
import { User } from "../models/User.js"

// SÄKERHET [Krav 4]: Middleware som skyddar alla privata routes
// Verifierar att en giltig JWT-token skickats med i Authorization-headern
// Används som grindvakt vid övergången Zon 1 → Zon 2 (frontend → server)
export const authenticateUser = async (req, res, next) => {
const token = req.headers.authorization?.replace("Bearer ", "")

// SÄKERHET: Om ingen token finns nekas åtkomst direkt med 401
if (!token) {
return res.status(401).json({ success: false, message: "No token provided" })
}

try {
// SÄKERHET: jwt.verify kontrollerar både signatur och utgångstid (expiresIn)
// Om token manipulerats eller gått ut kastas ett fel och användaren nekas åtkomst
const decoded = jwt.verify(token, process.env.JWT_SECRET)

const user = await User.findById(decoded.userId)

// SÄKERHET: Kontrollerar att användaren fortfarande finns i databasen
// Skyddar mot fall där ett konto raderats men en gammal token fortfarande används
if (!user) {
return res.status(401).json({ success: false, message: "User not found" })
}

// Lägger användaren på req.user så att routes kan kontrollera ägarskap
req.user = user
next()
} catch (err) {
// SÄKERHET: Alla JWT-fel (utgången token, ogiltig signatur) returnerar 401
res.status(401).json({ success: false, message: "Invalid token" })
}
}
3 changes: 3 additions & 0 deletions backend/models/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ const userSchema = new mongoose.Schema({
unique: true,
lowercase: true,
},
// SÄKERHET [Krav 2]: Lösenordet lagras alltid som bcrypt-hash, aldrig i klartext
// Styrkevalidering sker i server.js innan hashning – schemat lagrar bara resultatet
// Skyddar mot Information Disclosure om databasen skulle läcka (STRIDE: Information Disclosure)
password: {
type: String,
required: true,
Expand Down
16 changes: 16 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-list-endpoints": "^6.0.0",
"express-rate-limit": "^7.5.0",
"helmet": "^7.1.0",
"jsonwebtoken": "8.5.1",
"mongoose": "^8.4.0",
Expand Down
68 changes: 64 additions & 4 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,40 @@ import express from "express"
import mongoose from "mongoose"
import bcrypt from "bcrypt"
import jwt from "jsonwebtoken"
import rateLimit from "express-rate-limit" // SÄKERHET [Krav 3]: Importerar rate limiting-paketet
import { Message } from "./models/Message.js"
import { User } from "./models/User.js"
import { authenticateUser } from "./middleware/auth.js"
import "./config/db.js"
import listEndpoints from "express-list-endpoints"

// SÄKERHET: Servern startar inte utan JWT_SECRET – förhindrar att tokensignering sker med undefined-nyckel
if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env")

const PORT = process.env.PORT || "3000"
const app = express()

// SÄKERHET: Helmet sätter säkra HTTP-headers (t.ex. X-Frame-Options, Content-Security-Policy)
// Skyddar mot vanliga webbattacker som clickjacking och XSS via headers
app.use(helmet())

// SÄKERHET [Krav 4 – risk]: CORS tillåter alla origins med "*"
// I produktion bör detta begränsas till bara frontenddomänen, t.ex. origin: "https://din-app.se"
app.use(cors({
origin: "*",
}))
app.use(express.json())

// SÄKERHET [Krav 3]: Rate limiting på inloggning – max 10 försök per 15 minuter per IP
// Skyddar mot brute force-attacker (STRIDE: Spoofing, DoS) vid övergången Zon 0 → Zon 1
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minuter
max: 10,
message: { success: false, message: "Too many login attempts, please try again later" },
standardHeaders: true,
legacyHeaders: false,
})

app.get("/", (req, res) => {
res.send(listEndpoints(app))
})
Expand All @@ -33,6 +51,17 @@ app.post("/register", async (req, res) => {
return res.status(400).json({ success: false, message: "Username must be at least 2 characters" })
}

// SÄKERHET [Krav 2]: Lösenordsvalidering – kontrollerar styrka innan hashning
// Krav: minst 8 tecken, minst en siffra, minst ett specialtecken
// Skyddar mot svaga lösenord som är enkla att gissa (STRIDE: Spoofing)
const passwordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*])[A-Za-z0-9!@#$%^&*]{8,}$/
if (!password || !passwordRegex.test(password)) {
return res.status(400).json({
success: false,
message: "Password must be at least 8 characters and contain at least one number and one special character (!@#$%^&*)",
})
}

const existingUser = await User.findOne({
$or: [{ email: email.toLowerCase() }, { username: username.trim() }]
})
Expand All @@ -45,14 +74,18 @@ app.post("/register", async (req, res) => {
})
}

// SÄKERHET [Krav 2]: bcrypt med kostnadsfaktor 10 – lösenordet lagras aldrig i klartext
// Även om databasen läcker kan lösenorden inte läsas direkt (STRIDE: Information Disclosure)
const hashedPassword = await bcrypt.hash(password, 10)
const user = new User({ username: username.trim(), email, password: hashedPassword })
await user.save()

// SÄKERHET [Krav 1]: Token giltig i 30 minuter – användaren loggas ut automatiskt efter det
// Minskar risken om någon lämnar datorn obevakad (STRIDE: Spoofing, Elevation of Privilege)
const accessToken = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
{ expiresIn: "30m" }
)

res.status(201).json({
Expand All @@ -73,7 +106,8 @@ app.post("/register", async (req, res) => {
}
})

app.post("/login", async (req, res) => {
// SÄKERHET [Krav 3]: loginLimiter appliceras här – begränsar inloggningsförsök per IP
app.post("/login", loginLimiter, async (req, res) => {
try {
const { login, password } = req.body
const user = await User.findOne({
Expand All @@ -97,10 +131,11 @@ app.post("/login", async (req, res) => {
})
}

// SÄKERHET [Krav 1]: Token giltig i 30 minuter – uppfyller kravet om automatisk utloggning
const accessToken = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
{ expiresIn: "30m" }
)

res.json({
Expand All @@ -123,8 +158,14 @@ app.post("/login", async (req, res) => {

const isValidId = (id) => mongoose.Types.ObjectId.isValid(id)

// SÄKERHET [Krav 4 – notering]: GET /messages kräver ingen inloggning
// Alla kan läsa meddelanden – om appen ska vara privat bör authenticateUser läggas till här
app.get("/messages", async (req, res) => {
try {
// 🔐 SÄKERHETSKRAV 4:
// Denna route saknar authenticateUser → alla kan läsa alla meddelanden.
// Kravet säger att användaren måste vara inloggad för att göra något.
// I fas 3 bör authenticateUser läggas här.
const messages = await Message.find()
.sort({ createdAt: "desc" })
.limit(20)
Expand All @@ -136,6 +177,8 @@ app.get("/messages", async (req, res) => {
}
})

// SÄKERHET [Krav 4]: authenticateUser krävs – bara inloggade användare kan skapa meddelanden
// Skyddar Zon 1 → Zon 2 (STRIDE: Tampering, Elevation of Privilege)
app.post("/messages", authenticateUser, async (req, res) => {
const message = new Message({ message: req.body.message, user: req.user._id })
try {
Expand All @@ -146,6 +189,8 @@ app.post("/messages", authenticateUser, async (req, res) => {
}
})

// SÄKERHET [Krav 4]: authenticateUser + ägarskontroll – användaren kan bara redigera sina egna meddelanden
// message.user.toString() jämförs med req.user._id för att verifiera ägarskap (STRIDE: Tampering)
app.patch("/messages/:id", authenticateUser, async (req, res) => {
if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" })
try {
Expand All @@ -165,11 +210,26 @@ app.patch("/messages/:id", authenticateUser, async (req, res) => {
}
})

app.delete("/messages/:id", async (req, res) => {
// SÄKERHET [Krav 4 – fix]: authenticateUser och ägarskontroll tillagda
// Originalfilen saknade helt auth här – vem som helst kunde radera vilket meddelande som helst
// utan att ens vara inloggad. Det är ett kritiskt säkerhetshål (STRIDE: Tampering,
// Elevation of Privilege, Zon 1 → Zon 2).
//
// Två separata säkerhetslager:
// 1. Autentisering (authenticateUser): Är du inloggad? – uppfyller Krav 4
// 2. Auktorisering (ägarskontroll): Har du rätt att radera just det här meddelandet?
// Konsekvent med hur PATCH redan fungerade i originalfilen.
app.delete("/messages/:id", authenticateUser, async (req, res) => {
if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" })
try {
const message = await Message.findById(req.params.id)
if (!message) return res.status(404).json({ error: "Message not found" })

// SÄKERHET [Krav 4]: Ägarskontroll – användaren kan bara radera sina egna meddelanden
if (message.user.toString() !== req.user._id.toString()) {
return res.status(403).json({ error: "You can only delete your own messages" })
}

await message.deleteOne()
res.status(204).send()
} catch (error) {
Expand Down
45 changes: 39 additions & 6 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useState, useEffect } from "react"
import { useState, useEffect, useCallback } from "react"
import { BASE_URL } from "./api"
import { PostMessage } from "./components/PostMessage"
import { MessageList } from "./components/MessageList"
import { AuthModal } from "./components/AuthModal"

const INACTIVITY_TIMEOUT_MS = 30 * 60 * 1000
const ACTIVITY_EVENTS = ["click", "keydown", "scroll", "mousemove", "touchstart"]

export const App = () => {
const [loading, setLoading] = useState(false)
const [messageList, setMessageList] = useState([])
Expand All @@ -23,22 +26,51 @@ export const App = () => {
fetchPosts()
}, [])

const addNewPost = (newMessage) => {
const addNewPost = (newMessage) => {
setMessageList([newMessage, ...messageList])
}

const handleUnauthorized = () => {
const logout = useCallback((message = null) => {
setUser(null)
setError("Your session has expired, please log in again")
}
setError(message)
}, [])

const handleUnauthorized = useCallback(() => {
logout("Your session has expired, please log in again")
}, [logout])

useEffect(() => {
if (!user) return undefined

let inactivityTimer

const resetInactivityTimer = () => {
clearTimeout(inactivityTimer)
inactivityTimer = setTimeout(() => {
logout("You were logged out after 30 minutes of inactivity")
}, INACTIVITY_TIMEOUT_MS)
}

resetInactivityTimer()
ACTIVITY_EVENTS.forEach((eventName) => {
window.addEventListener(eventName, resetInactivityTimer, { passive: true })
})

return () => {
clearTimeout(inactivityTimer)
ACTIVITY_EVENTS.forEach((eventName) => {
window.removeEventListener(eventName, resetInactivityTimer)
})
}
}, [user, logout])

return (
<>
{user ? (
<div className="user-info">
<span>{user.response.username}</span>
<button
onClick={() => setUser(null)}
onClick={() => logout()}
className="auth-button"
>
Logout
Expand Down Expand Up @@ -67,6 +99,7 @@ export const App = () => {
onSuccess={(data) => {
console.log("User logged in:", data)
setUser(data)
setError(null)
setModal(null)
}}
/>
Expand Down
8 changes: 7 additions & 1 deletion granskningsfasen.md
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
# Inlämning 3 - Granskningsfasen
# Inlämning 3 - Granskningsfasen
Säkerhetsgranskning – Sammanfattning
Vi genomförde en säkerhetsgranskning av YH Message App med hjälp av verktygen CodeQL och Dependabot. Syftet var att kontrollera om de säkerhetskrav som identifierades under hotmodelleringen hade implementerats korrekt samt att hitta eventuella säkerhetsbrister.
Granskningen visade att flera viktiga säkerhetsåtgärder redan finns på plats. Applikationen använder bcrypt för säker lagring av lösenord, JWT för autentisering, rate limiting på inloggningen samt autentisering och ägarskontroll för redigering och radering av meddelanden. Även Helmet används för att stärka säkerheten i HTTP-headers.
CodeQL identifierade dock några förbättringsområden. Den viktigaste bristen var att routen GET /messages saknar autentisering, vilket innebär att meddelanden kan läsas utan inloggning. Flera routes saknar också rate limiting, framför allt registreringen, vilket kan göra systemet mer sårbart för automatiserade anrop och belastningsattacker. Dessutom använder applikationen en för tillåtande CORS-konfiguration genom origin: "*".
Dependabot hittade flera sårbarheter i externa beroenden. Det mest relevanta paketet var jsonwebtoken, eftersom det används direkt i applikationens autentisering. Även andra paket bör uppdateras för att minska potentiella säkerhetsrisker.
Sammanfattningsvis bedöms applikationen ha en bra säkerhetsgrund, men säkerheten kan förbättras genom att skydda GET /messages med autentisering, införa mer konsekvent rate limiting, begränsa CORS till den avsedda frontenddomänen samt uppdatera sårbara beroenden.
Binary file added image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions kodguide-fil-for-fil.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Vad filen gör:
- Kopplar backend till MongoDB med Mongoose.
- Loggar när anslutningen fungerar eller ger fel.


Exempel:
```js
const mongoUrl = process.env.MONGO_URL || "mongodb://localhost/messages"
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 24 additions & 1 deletion planeringsfasen.md
Original file line number Diff line number Diff line change
@@ -1 +1,24 @@
# Inlämning 1 - Planeringsfasen
# Inlämning 1 - Planeringsfasen
Vi har gjort en säkerhetsgranskning av YH Message App med hjälp av hotmodellering. Det betyder att vi försöker hitta risker i systemet innan någon angripare gör det — ungefär som att tänka som en tjuv för att kunna bygga bättre lås.

Först gick vi igenom vad hotmodellering är:

förstå systemet, tänka igenom vad som kan gå fel, bestämma hur vi skyddar oss och senare kontrollera att lösningarna fungerar

Vi beskrev sedan systemarkitekturen: webbläsare → frontend → server → databas.
Varje del är en möjlig angreppspunkt.

Med STRIDE‑modellen såg vi vilka typer av hot som finns, som spoofing, datamanipulation, dataläckor och överbelastning.
Med ESTRID‑modellen såg vi var hoten är störst — i övergångarna mellan zonerna, där servern och databasen är mest skyddsvärda.

Utifrån detta tog vi fram fyra enkla säkerhetskrav:

1. Automatisk utloggning

2. Starka och säkert sparade lösenord

3. Rate limiting för att stoppa brute force

4. Endast inloggade användare får göra något

Tillsammans ger detta en tydlig säkerhetsgrund inför nästa steg: kodgranskning och säkerhetstestning.
Loading