diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 00000000..ef82fbc6 Binary files /dev/null and b/.DS_Store differ diff --git a/README.md b/README.md index d6aeeb20..e7780303 100644 --- a/README.md +++ b/README.md @@ -1 +1,25 @@ -# yh-message-app-fullstack \ No newline at end of file +# yh-message-app-fullstack +## Security Improvements + +### Authentication + +Added authentication middleware to protected endpoints. + +### Authorization + +Added ownership validation before deleting messages. + +### Input Validation + +Added minlength and maxlength validation for messages. + +### OWASP Mapping + +* A01 Broken Access Control +* A05 Security Misconfiguration + +### Security Principles + +* Least Privilege +* Defense in Depth +* Secure Authentication diff --git a/backend/.env.example b/backend/.env.example index d417a785..fdf06434 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,4 +1,4 @@ PORT=3000 MONGO_URL=connection-string-from-mongodb-atlas -JWT_SECRET=your-secret-here -FRONTEND_URL=http://localhost:5500 +JWT_SECRET=YH2026SuperSecretKey +FRONTEND_URL=http://localhost:3000 diff --git a/backend/models/Message.js b/backend/models/Message.js index dbca0001..4d2debda 100644 --- a/backend/models/Message.js +++ b/backend/models/Message.js @@ -3,16 +3,24 @@ import mongoose from "mongoose" const messageSchema = new mongoose.Schema({ message: { type: String, - required: true + required: true, + // SÄKERHETSFIX: Längdvalidering på schemanivå (serversidan). + // Klientvalidering kan alltid kringgås av en angripare som skickar + // förfrågningar direkt mot API:et. Genom att validera här säkerställs + // att inga meddelanden kortare än 3 eller längre än 140 tecken sparas, + // oavsett varifrån förfrågan kommer. + // (Säkerhetskrav 4: Input Validation, STRIDE: Tampering) + minlength: 3, + maxlength: 140, }, user: { type: mongoose.Schema.Types.ObjectId, ref: "User", }, -createdAt: { + createdAt: { type: Date, default: Date.now, }, }) -export const Message = mongoose.model("Message", messageSchema) +export const Message = mongoose.model("Message", messageSchema) \ No newline at end of file diff --git a/backend/server.js b/backend/server.js index c8d0c218..ec301646 100644 --- a/backend/server.js +++ b/backend/server.js @@ -11,28 +11,47 @@ import { authenticateUser } from "./middleware/auth.js" import "./config/db.js" import listEndpoints from "express-list-endpoints" +// SÄKERHET: Applikationen startar inte om JWT_SECRET saknas i miljövariablerna. +// Detta förhindrar att JWT-tokens signeras med ett tomt eller förutsägbart värde, +// vilket annars skulle möjliggöra förfalskning av tokens (STRIDE: Spoofing). 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 automatiskt säkerhetsrelaterade HTTP-headers, +// t.ex. X-Content-Type-Options, X-Frame-Options och Content-Security-Policy. +// Detta är en del av "Defense in Depth" – flera lager av skydd. app.use(helmet()) + +// OBS: cors({ origin: "*" }) tillåter anrop från alla domäner. +// I produktion bör detta begränsas till specifika betrodda origins, +// t.ex. origin: "https://din-app.com", för att minska risken för +// obehöriga cross-origin-anrop (STRIDE: Elevation of Privilege). app.use(cors({ origin: "*", })) + app.use(express.json()) app.get("/", (req, res) => { res.send(listEndpoints(app)) }) +// --- REGISTRERING --- app.post("/register", async (req, res) => { try { const { email, password, username } = req.body + // SÄKERHET: Validering av användarnamn på serversidan. + // Klientvalidering kan kringgås – backend måste alltid validera själv. + // (Säkerhetskrav: Input Validation, STRIDE: Tampering) if (!username || username.trim().length < 2) { return res.status(400).json({ success: false, message: "Username must be at least 2 characters" }) } + // SÄKERHET: Kontrollerar om email eller användarnamn redan finns + // innan ett nytt konto skapas, för att förhindra duplicerade identiteter. const existingUser = await User.findOne({ $or: [{ email: email.toLowerCase() }, { username: username.trim() }] }) @@ -45,10 +64,16 @@ app.post("/register", async (req, res) => { }) } + // SÄKERHET: Lösenordet hashas med bcrypt (saltfaktor 10) innan det lagras. + // Lösenordet sparas ALDRIG i klartext i databasen. + // (Säkerhetskrav: Secure Authentication, 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: JWT-token signeras med JWT_SECRET och löper ut efter 2 timmar. + // Begränsad livslängd minskar risken om en token skulle läcka. + // (Säkerhetskrav: Secure Authentication, STRIDE: Spoofing) const accessToken = jwt.sign( { userId: user._id, username: user.username }, process.env.JWT_SECRET, @@ -73,9 +98,16 @@ app.post("/register", async (req, res) => { } }) +// --- INLOGGNING --- app.post("/login", async (req, res) => { try { const { login, password } = req.body + + // SÄKERHET: Tillåter inloggning med både användarnamn och email. + // Returnerar samma felmeddelande oavsett om felet beror på fel + // användarnamn eller fel lösenord – detta förhindrar "user enumeration" + // där en angripare annars kan lista ut vilka konton som finns. + // (STRIDE: Information Disclosure) const user = await User.findOne({ $or: [{ username: login }, { email: login }] }) @@ -88,6 +120,8 @@ app.post("/login", async (req, res) => { }) } + // SÄKERHET: bcrypt.compare() jämför lösenordet mot det lagrade hashvärdet + // utan att avslöja det faktiska lösenordet. const passwordMatch = await bcrypt.compare(password, user.password) if (!passwordMatch) { return res.status(401).json({ @@ -123,6 +157,7 @@ app.post("/login", async (req, res) => { const isValidId = (id) => mongoose.Types.ObjectId.isValid(id) +// --- HÄMTA MEDDELANDEN (publik endpoint) --- app.get("/messages", async (req, res) => { try { const messages = await Message.find() @@ -136,7 +171,13 @@ app.get("/messages", async (req, res) => { } }) +// --- SKAPA MEDDELANDE --- +// SÄKERHET: authenticateUser-middleware kontrollerar att användaren är +// inloggad via giltig JWT innan meddelandet sparas. +// (Säkerhetskrav: Access Control, STRIDE: Spoofing/Elevation of Privilege) app.post("/messages", authenticateUser, async (req, res) => { + // NOTERING: Längdvalidering (3–140 tecken) hanteras i Message-modellen via + // minlength/maxlength på schemanivå. Se models/Message.js. const message = new Message({ message: req.body.message, user: req.user._id }) try { const saved = await message.save() @@ -146,12 +187,18 @@ app.post("/messages", authenticateUser, async (req, res) => { } }) +// --- REDIGERA MEDDELANDE --- app.patch("/messages/:id", authenticateUser, async (req, res) => { + // SÄKERHET: Validerar att ID:t är ett giltigt MongoDB ObjectId-format + // för att undvika NoSQL-injektionsrisker och onödiga databasfrågor. 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: Kontrollerar att den inloggade användaren äger meddelandet + // innan redigering tillåts. Förhindrar att användare redigerar andras meddelanden. + // (Säkerhetskrav 1 & 3: Access Control, STRIDE: Tampering) if (message.user.toString() !== req.user._id.toString()) { return res.status(403).json({ error: "You can only edit your own messages" }) } @@ -165,11 +212,23 @@ app.patch("/messages/:id", authenticateUser, async (req, res) => { } }) -app.delete("/messages/:id", async (req, res) => { +// --- RADERA MEDDELANDE --- +// SÄKERHETSFIX: authenticateUser tillagt – tidigare saknades autentisering helt, +// vilket innebar att vem som helst kunde radera vilket meddelande som helst +// utan att vara inloggad. (Säkerhetskrav 1 & 3, STRIDE: Tampering/Elevation of Privilege) +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ÄKERHETSFIX: Kontrollerar att den inloggade användaren äger meddelandet + // innan radering tillåts. Förhindrar att användare raderar andras meddelanden. + // (Säkerhetskrav 1: Access Control, STRIDE: Tampering) + 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) { @@ -179,4 +238,4 @@ app.delete("/messages/:id", async (req, res) => { app.listen(PORT, () => { console.log(`Listening on port ${PORT}`) -}) +}) \ No newline at end of file diff --git a/frontend/src/components/SingleMessage.jsx b/frontend/src/components/SingleMessage.jsx index 50d2274b..b1586e53 100644 --- a/frontend/src/components/SingleMessage.jsx +++ b/frontend/src/components/SingleMessage.jsx @@ -6,6 +6,10 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) => const [editedText, setEditedText] = useState(message.message) const [editError, setEditError] = useState("") + // SÄKERHET: isOwner kontrollerar om den inloggade användaren äger meddelandet. + // Används för att styra vilka UI-kontroller som visas – en användare ska + // aldrig se redigera/radera-knappar för andras meddelanden. + // (Säkerhetskrav 1: Access Control / Least Privilege) const isOwner = user && user.response.id === message.user?._id const onDelete = async () => { @@ -13,6 +17,8 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) => const res = await fetch(`${BASE_URL}/messages/${message._id}`, { method: "DELETE", headers: { + // SÄKERHET: JWT-token skickas med i Authorization-headern. + // Backend verifierar tokenen och ägarskapet innan radering sker. Authorization: `Bearer ${user?.response?.accessToken}`, }, }) @@ -83,7 +89,14 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) => )}
- + {/* SÄKERHETSFIX: Radera-knappen visas nu endast för meddelandets ägare. + Tidigare visades knappen för alla inloggade användare i UI:t. + Även om backend nu blockerar obehörig radering är det god praxis + att inte visa kontroller som användaren inte har rätt att använda. + (Säkerhetskrav 1: Least Privilege / Access Control) */} + {isOwner && ( + + )} {isOwner && !isEditing && ( diff --git a/granskningsfasen.md b/granskningsfasen.md index 088f9718..c8435a8e 100644 --- a/granskningsfasen.md +++ b/granskningsfasen.md @@ -1 +1 @@ -# Inlämning 3 - Granskningsfasen \ No newline at end of file +# Inlämning 3 - Granskningsfasen diff --git a/planeringsfasen.md b/planeringsfasen.md index 661cae0e..feba695f 100644 --- a/planeringsfasen.md +++ b/planeringsfasen.md @@ -1 +1,72 @@ -# Inlämning 1 - Planeringsfasen \ No newline at end of file +# Inlämning 1 - Planeringsfasen +# Fas 1 – Planering + +## Projekt + +Webbapplikationen låter användare: + +* Skapa konto +* Logga in +* Skapa meddelanden +* Redigera meddelanden +* Ta bort meddelanden + +--- + +## Hotmodellering (STRIDE) + +### Spoofing + +Angripare försöker utge sig för att vara en annan användare. + +### Tampering + +Manipulation av meddelanden eller annan data. + +### Repudiation + +Användare kan förneka utförda handlingar. + +### Information Disclosure + +Känslig information som lösenord eller användardata exponeras. + +### Denial of Service + +Systemet överbelastas med stora mängder förfrågningar. + +### Elevation of Privilege + +En användare får högre behörigheter än avsett. + +--- + +## Säkerhetskrav + +1. Användare får endast redigera och ta bort sina egna meddelanden. +2. Lösenord ska hashats och aldrig lagras i klartext. +3. Endast autentiserade användare får skapa, redigera eller ta bort meddelanden. +4. Meddelanden ska valideras och vara mellan 3–140 tecken. +5. Användargenererat innehåll ska skyddas mot XSS genom sanitization eller escaping. + +--- + +## Säkerhetsprinciper + +* Least Privilege +* Defense in Depth +* Secure Authentication +* Input Validation +* Access Control + +--- + +## Slutsats + +Genom STRIDE-modellen identifierades flera säkerhetsrisker kopplade till autentisering, åtkomstkontroll och användargenererat innehåll. Säkerhetskraven ovan har definierats för att minska dessa risker och kommer att användas som grund för kodanalys och granskning i kommande faser. + +## Presentation + +Här är min presentation: + +[Öppna presentationen](https://mull-extra-13464102.figma.site)