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
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
# yh-message-app-fullstack
# Säker systemutveckling

Det här projektet är en del av kursen Säker systemutveckling och bygger på en redan existerande fullstack-applikation för meddelanden som finns på GitHub.
Applikationen låter användaren:

- Skapa ett konto
- Logga in
- Skapa meddelanden
- Redigera meddelanden
- Ta bort meddelanden

Syftet med projektet är inte att bygga en helt ny applikation från grunden, utan istället att analysera och förstå en existerande applikation ur ett säkerhetsperspektiv.
Uppgiften är uppdelad i tre faser:

1.Planering

2.Kodförståelse

3.Säkerhetsgranskning
215 changes: 135 additions & 80 deletions backend/server.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,82 @@
import "dotenv/config"
import helmet from "helmet"
import cors from "cors"
import express from "express"
import mongoose from "mongoose"
import bcrypt from "bcrypt"
import jwt from "jsonwebtoken"
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"

if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env")

const PORT = process.env.PORT || "3000"
const app = express()
app.use(helmet())
app.use(cors({
origin: "*",
}))
app.use(express.json())
import "dotenv/config";
import helmet from "helmet";
import cors from "cors";
import express from "express";
import mongoose from "mongoose";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
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";

if (!process.env.JWT_SECRET) throw new Error("JWT_SECRET is not set in .env");

const PORT = process.env.PORT || "3000";
const app = express();
app.use(helmet());
app.use(
cors({
origin: "*",
}),
);
app.use(express.json());

app.get("/", (req, res) => {
res.send(listEndpoints(app))
})
res.send(listEndpoints(app));
});

app.post("/register", async (req, res) => {
try {
const { email, password, username } = req.body
const { email, password, username } = req.body;

// SECURITY FIX:
// Passwords should not be too weak.
// This checks minimum length and requires letters and numbers.
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d).{8,}$/;

if (!password || !passwordRegex.test(password)) {
return res.status(400).json({
success: false,
message:
"Password must be at least 8 characters and include both letters and numbers",
});
}

if (!username || username.trim().length < 2) {
return res.status(400).json({ success: false, message: "Username must be at least 2 characters" })
return res.status(400).json({
success: false,
message: "Username must be at least 2 characters",
});
}

const existingUser = await User.findOne({
$or: [{ email: email.toLowerCase() }, { username: username.trim() }]
})
$or: [{ email: email.toLowerCase() }, { username: username.trim() }],
});

if (existingUser) {
const field = existingUser.email === email.toLowerCase() ? "email" : "username"
const field =
existingUser.email === email.toLowerCase() ? "email" : "username";
return res.status(400).json({
success: false,
message: `A user with this ${field} already exists`
})
message: `A user with this ${field} already exists`,
});
}

const hashedPassword = await bcrypt.hash(password, 10)
const user = new User({ username: username.trim(), email, password: hashedPassword })
await user.save()
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({
username: username.trim(),
email,
password: hashedPassword,
});
await user.save();

const accessToken = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
)
{ expiresIn: "2h" },
);

res.status(201).json({
success: true,
Expand All @@ -63,45 +86,45 @@ app.post("/register", async (req, res) => {
id: user._id,
accessToken,
},
})
});
} catch (error) {
res.status(400).json({
success: false,
message: "Could not create user",
error: error,
})
});
}
})
});

app.post("/login", async (req, res) => {
try {
const { login, password } = req.body
const { login, password } = req.body;
const user = await User.findOne({
$or: [{ username: login }, { email: login }]
})
$or: [{ username: login }, { email: login }],
});

if (!user) {
return res.status(401).json({
success: false,
message: "No account found with that username or email",
response: null,
})
});
}

const passwordMatch = await bcrypt.compare(password, user.password)
const passwordMatch = await bcrypt.compare(password, user.password);
if (!passwordMatch) {
return res.status(401).json({
success: false,
message: "Password is incorrect",
response: null,
})
});
}

const accessToken = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: "2h" }
)
{ expiresIn: "2h" },
);

res.json({
success: true,
Expand All @@ -111,72 +134,104 @@ app.post("/login", async (req, res) => {
id: user._id,
accessToken,
},
})
});
} catch (error) {
res.status(500).json({
success: false,
message: "Something went wrong",
error: error,
})
});
}
})
});

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

app.get("/messages", async (req, res) => {
try {
const messages = await Message.find()
.sort({ createdAt: "desc" })
.limit(20)
.populate("user", "username")
.exec()
res.json(messages)
.exec();
res.json(messages);
} catch (error) {
res.status(500).json({ message: "Could not fetch messages" })
res.status(500).json({ message: "Could not fetch messages" });
}
})
});

app.post("/messages", authenticateUser, async (req, res) => {
const message = new Message({ message: req.body.message, user: req.user._id })
const { message } = req.body;

// SECURITY FIX:
// Validate user input before saving it to the database.
// This prevents empty messages and limits very long input.
if (typeof message !== "string") {
return res.status(400).json({ error: "Message must be text" });
}

const trimmedMessage = message.trim();

if (trimmedMessage.length < 1 || trimmedMessage.length > 200) {
return res.status(400).json({
error: "Message must be between 1 and 200 characters",
});
}

const newMessage = new Message({
message: trimmedMessage,
user: req.user._id,
});

try {
const saved = await message.save()
res.status(201).json(saved)
const saved = await newMessage.save();
res.status(201).json(saved);
} catch (err) {
res.status(400).json({ message: "Could not save message", errors: err.errors })
res
.status(400)
.json({ message: "Could not save message", errors: err.errors });
}
})
});

app.patch("/messages/:id", authenticateUser, async (req, res) => {
if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" })
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" })
const message = await Message.findById(req.params.id);
if (!message) return res.status(404).json({ error: "Message not found" });

if (message.user.toString() !== req.user._id.toString()) {
return res.status(403).json({ error: "You can only edit your own messages" })
return res
.status(403)
.json({ error: "You can only edit your own messages" });
}

message.message = req.body.editedMessage
await message.save()
const updated = await message.populate("user", "username")
res.json(updated)
message.message = req.body.editedMessage;
await message.save();
const updated = await message.populate("user", "username");
res.json(updated);
} catch (error) {
res.status(400).json({ error: "Could not update message" })
res.status(400).json({ error: "Could not update message" });
}
})
});

app.delete("/messages/:id", async (req, res) => {
if (!isValidId(req.params.id)) return res.status(400).json({ error: "Invalid message ID" })
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" })
await message.deleteOne()
res.status(204).send()
const message = await Message.findById(req.params.id);
if (!message) return res.status(404).json({ error: "Message not found" });
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) {
res.status(400).json({ error: "Could not delete message" })
res.status(400).json({ error: "Could not delete message" });
}
})
});

app.listen(PORT, () => {
console.log(`Listening on port ${PORT}`)
})
console.log(`Listening on port ${PORT}`);
});
3 changes: 2 additions & 1 deletion frontend/src/api.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const BASE_URL = "https://yh-message-app-fullstack.onrender.com"
//export const BASE_URL = "https://yh-message-app-fullstack.onrender.com"
export const BASE_URL = "http://localhost:3000";
Loading