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
Binary file added .DS_Store
Binary file not shown.
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
# yh-message-app-fullstack
# 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
4 changes: 2 additions & 2 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -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
14 changes: 11 additions & 3 deletions backend/models/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
63 changes: 61 additions & 2 deletions backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() }]
})
Expand All @@ -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,
Expand All @@ -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 }]
})
Expand All @@ -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({
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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" })
}
Expand All @@ -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) {
Expand All @@ -179,4 +238,4 @@ app.delete("/messages/:id", async (req, res) => {

app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`)
})
})
15 changes: 14 additions & 1 deletion frontend/src/components/SingleMessage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ 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 () => {
try {
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}`,
},
})
Expand Down Expand Up @@ -83,7 +89,14 @@ export const SingleMessage = ({ message, user, onUnauthorized, fetchPosts }) =>
)}

<div className="message-actions">
<button type="button" className="delete-btn" onClick={onDelete}>🗑️</button>
{/* 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 && (
<button type="button" className="delete-btn" onClick={onDelete}>🗑️</button>
)}

{isOwner && !isEditing && (
<button type="button" className="edit-btn" onClick={() => setIsEditing(true)}>✏️</button>
Expand Down
2 changes: 1 addition & 1 deletion granskningsfasen.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# Inlämning 3 - Granskningsfasen
# Inlämning 3 - Granskningsfasen
73 changes: 72 additions & 1 deletion planeringsfasen.md
Original file line number Diff line number Diff line change
@@ -1 +1,72 @@
# Inlämning 1 - Planeringsfasen
# 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)