Skip to content

Commit 2ae1538

Browse files
committed
* fixed cleanup
* secured path traversal * added hint for stegano vuln
1 parent 970040f commit 2ae1538

File tree

10 files changed

+55
-30
lines changed

10 files changed

+55
-30
lines changed

checker/src/checker.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ async def upload_image(self, token, flag):
162162
try:
163163
process = await asyncio.create_subprocess_exec(
164164
"steghide", "embed",
165-
"-cf", "goblin.jpg",
165+
"-cf", "stegano.jpg",
166166
"-ef", flagfile_path,
167167
"-sf", steghide_img_path,
168168
"-p", "",
@@ -177,7 +177,7 @@ async def upload_image(self, token, flag):
177177
raise MumbleException(f"Steghide failed: {error_message}")
178178

179179
async with aiofiles.open(steghide_img_path, "rb") as f:
180-
files = {"file": ("goblin.jpg", await f.read(), "image/jpeg")}
180+
files = {"file": ("stegano.jpg", await f.read(), "image/jpeg")}
181181
resp = await self.client.post(
182182
"/images/upload",
183183
files=files,
@@ -688,7 +688,7 @@ async def exploit_image(
688688
con = Connection(logger, client)
689689
username = rand_str(8)
690690
password = "CascwTdwsaj"
691-
filename = f"%2e%2e/{task.attack_info}/goblin.jpg"
691+
filename = f"%2e%2e/{task.attack_info}/stegano.jpg"
692692
try:
693693
token = await con.register_and_login(username, password)
694694
flag = rand_str(32)

checker/src/goblin.jpg

-13 KB
Binary file not shown.

checker/src/stegano.jpg

71.3 KB
Loading

service/backend/crud.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ async def get_dungeons(db: AsyncSession, skip: int = 0, limit: int = 100):
110110
)
111111
return result.scalars().all()
112112

113-
async def record_completed_dungeon(db: AsyncSession, user_id: int, dungeon_id: int):
113+
async def record_completed_dungeon(db, user_id: int, dungeon_id: int):
114114
result = await db.execute(
115115
select(models.CompletedDungeon).where(
116116
models.CompletedDungeon.user_id == user_id,
@@ -119,7 +119,7 @@ async def record_completed_dungeon(db: AsyncSession, user_id: int, dungeon_id: i
119119
)
120120
db_completed_dungeon = result.scalar_one_or_none()
121121

122-
now = datetime.datetime.now().isoformat()
122+
now = datetime.datetime.utcnow() # Use UTC for consistency
123123

124124
if db_completed_dungeon:
125125
db_completed_dungeon.completion_timestamp = now

service/backend/database.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,23 @@
55

66
DATABASE_URL = "postgresql+asyncpg://backend:backend@backend-db:5432/backenddb"
77

8-
# Async Engine & Session
98
engine = create_async_engine(
109
DATABASE_URL,
1110
echo=True,
12-
pool_size=10, # this is important
13-
max_overflow=20, # and this too
11+
pool_size=10,
12+
max_overflow=20,
1413
)
1514
async_session = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
1615

1716
Base = declarative_base()
1817

19-
# Dependency for FastAPI (yield!)
2018
async def get_async_db():
2119
async with async_session() as session:
2220
try:
2321
yield session
2422
finally:
2523
await session.close()
2624

27-
# (OPTIONAL) Table creation for Startup
2825
async def init_db():
2926
async with engine.begin() as conn:
3027
await conn.run_sync(Base.metadata.create_all)

service/backend/main.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
@asynccontextmanager
3838
async def lifespan(app: FastAPI):
39-
# Init DB tables
4039
async with engine.begin() as conn:
4140
await conn.run_sync(models.Base.metadata.create_all)
4241

@@ -59,6 +58,10 @@ async def lifespan(app: FastAPI):
5958
name="Demon Tower",
6059
description="The legendary Demon. Hard challenge.",
6160
required_power=7500, xp_reward=500, image_name="demon.png", owner="all"))
61+
await crud.create_dungeon(db, schemas.DungeonCreate(
62+
name="Boss St. Egano",
63+
description="The legendary Boss. Ultimate challenge.",
64+
required_power=99999, xp_reward=9999, image_name="stegano.jpg", owner="all"))
6265
print("Initial dungeons created.")
6366
else:
6467
print("Dungeons already exist, skipping initial creation.")
@@ -70,7 +73,6 @@ async def lifespan(app: FastAPI):
7073
try:
7174
yield
7275
finally:
73-
# Optional: Task canceln (wenn du willst)
7476
task.cancel()
7577

7678

@@ -136,11 +138,8 @@ async def register(user: schemas.UserCreate, db=Depends(get_async_db)):
136138
if db_user:
137139
raise HTTPException(status_code=400, detail="Username (or Variation of it) is already taken!")
138140
try:
139-
# 1. User anlegen
140141
user_obj = await crud.create_user(db=db, user=user, username_key=normalform)
141-
# 2. User mit allen Items-Relationen nochmal laden (joinedload in crud!)
142142
user_full = await crud.get_user_by_username(db, user_obj.username)
143-
# 3. Jetzt das User-Objekt zurückgeben – Items sind garantiert geladen!
144143
return user_full
145144
except ValueError as e:
146145
raise HTTPException(status_code=400, detail=str(e))
@@ -172,7 +171,6 @@ async def read_users_me(token: str = Depends(oauth2_scheme), db=Depends(get_asyn
172171
user = result.unique().scalar_one_or_none()
173172
if not user:
174173
raise HTTPException(status_code=401, detail="User not found")
175-
# <<< HIER: Power mit Items summieren wie im Leaderboard!
176174
item_power = sum(item.bonus for item in user.items)
177175
user.power = user.power + item_power
178176
return user
@@ -187,7 +185,7 @@ async def create_item_for_user(item: schemas.ItemCreate, token: str = Depends(oa
187185
raise HTTPException(status_code=401, detail="Invalid credentials")
188186
return await crud.create_item_for_user(db=db, item=item, user_id=user.id)
189187

190-
@app.get("/users/", response_model=List[schemas.User]) # you can make a lighter schema for listing if needed
188+
@app.get("/users/", response_model=List[schemas.User])
191189
async def read_users(
192190
db=Depends(get_async_db),
193191
limit: int = 10,
@@ -593,16 +591,38 @@ async def cleanup_old_records():
593591
user upload folders asynchronously.
594592
"""
595593
while True:
596-
await asyncio.sleep(240)
594+
await asyncio.sleep(30)
597595
db_gen = get_async_db()
598596
db = await anext(db_gen)
599597
try:
600-
ten_minutes_ago = datetime.utcnow() - timedelta(minutes=10)
598+
ten_minutes_ago = datetime.utcnow() - timedelta(minutes=1)
601599

602600
users_to_delete_query = await db.execute(
603-
select(models.User.username).where(models.User.created_at < ten_minutes_ago)
601+
select(models.User.id, models.User.username).where(models.User.created_at < ten_minutes_ago)
604602
)
605-
deleted_usernames = [u[0] for u in users_to_delete_query.all()]
603+
deleted_users = users_to_delete_query.all()
604+
deleted_user_ids = [u[0] for u in deleted_users]
605+
deleted_usernames = [u[1] for u in deleted_users]
606+
607+
if deleted_user_ids:
608+
await db.execute(
609+
models.Inventory.__table__.delete().where(
610+
models.Inventory.user_id.in_(deleted_user_ids)
611+
)
612+
)
613+
614+
await db.execute(
615+
models.CompletedDungeon.__table__.delete().where(
616+
models.CompletedDungeon.user_id.in_(deleted_user_ids)
617+
)
618+
)
619+
620+
await db.execute(
621+
models.Fight.__table__.delete().where(
622+
(models.Fight.attacker_id.in_(deleted_user_ids)) |
623+
(models.Fight.defender_id.in_(deleted_user_ids))
624+
)
625+
)
606626

607627
await db.execute(
608628
models.Fight.__table__.delete().where(
@@ -613,7 +633,6 @@ async def cleanup_old_records():
613633
dungeons_subquery = select(models.Dungeon.id).where(
614634
(models.Dungeon.one_time_clear == False) | (models.Dungeon.one_time_clear == None)
615635
)
616-
617636
await db.execute(
618637
models.CompletedDungeon.__table__.delete().where(
619638
models.CompletedDungeon.completion_timestamp < ten_minutes_ago,
@@ -659,6 +678,7 @@ async def cleanup_old_records():
659678
await db_gen.aclose()
660679

661680

681+
662682
@app.post("/images/upload", status_code=status.HTTP_201_CREATED)
663683
async def upload_dungeon_image(file: UploadFile = File(...),
664684
current_user: schemas.User = Depends(get_current_active_user)):
@@ -684,10 +704,19 @@ async def upload_dungeon_image(file: UploadFile = File(...),
684704

685705
return {"filename": safe_filename, "detail": "Image uploaded successfully to user directory."}
686706

707+
UPLOADS_DIR = os.path.abspath("uploads")
708+
687709
@app.get("/images/{filename:path}")
688-
async def get_private_dungeon_image(filename: str, current_user: schemas.User = Depends(get_current_active_user)):
710+
async def get_private_dungeon_image(
711+
filename: str,
712+
current_user: schemas.User = Depends(get_current_active_user)
713+
):
714+
user_image_path = os.path.abspath(
715+
os.path.join(UPLOADS_DIR, current_user.username, filename)
716+
)
689717

690-
user_image_path = f"uploads/{current_user.username}/{filename}"
718+
if not user_image_path.startswith(UPLOADS_DIR + os.sep):
719+
raise HTTPException(status_code=404, detail="Image not found or you do not have permission to access it.")
691720

692721
if not os.path.exists(user_image_path):
693722
raise HTTPException(status_code=404, detail="Image not found or you do not have permission to access it.")

service/backend/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ class CompletedDungeon(Base):
7777
id = Column(Integer, primary_key=True, index=True)
7878
user_id = Column(Integer, ForeignKey("users.id"))
7979
dungeon_id = Column(Integer, ForeignKey("dungeons.id"))
80-
completion_timestamp = Column(String)
80+
completion_timestamp = Column(DateTime, default=datetime.utcnow, index=True)
8181
user = relationship("User", back_populates="completed_dungeons")
8282
dungeon = relationship("Dungeon")

service/backend/schemas.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ class CompletedDungeon(BaseModel):
9898
id: int
9999
user_id: int
100100
dungeon_id: int
101-
completion_timestamp: str
101+
completion_timestamp: datetime
102102

103103
class Config:
104104
orm_mode = True
71.3 KB
Loading

service/frontend/src/pages/DungeonsPage.jsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import AuthenticatedImage from '../components/AuthenticatedImage';
66
const DungeonsPage = () => {
77
const [dungeons, setDungeons] = useState([]);
88
const [message, setMessage] = useState('');
9-
const [user, setUser] = useState(null); // State to store the user object
9+
const [user, setUser] = useState(null);
1010
const [userPower, setUserPower] = useState(0);
1111
const API_URL = import.meta.env.VITE_API_URL;
1212
const navigate = useNavigate();
@@ -30,7 +30,7 @@ const DungeonsPage = () => {
3030
const fetchUser = async () => {
3131
try {
3232
const response = await getMe();
33-
setUser(response.data); // Store the whole user object
33+
setUser(response.data);
3434
setUserPower(response.data.power);
3535
} catch (error) {
3636
console.error('Error fetching user data:', error);
@@ -66,7 +66,6 @@ const DungeonsPage = () => {
6666
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
6767
{dungeons.length > 0 ? (
6868
dungeons.map((dungeon) => {
69-
// Use the user state and the correct property 'username'
7069
if (dungeon.owner === "all" || (user && dungeon.owner === user.username)) {
7170
let imageComponent = null;
7271
if (dungeon.image_name) {
@@ -84,7 +83,7 @@ const DungeonsPage = () => {
8483
imageComponent = (
8584
<img
8685
src={publicImageUrl}
87-
alt={dungeon.name}
86+
alt="Sometimes it's the things you can't see ;)"
8887
className="w-full h-80 object-cover rounded-md mb-4"
8988
/>
9089
);

0 commit comments

Comments
 (0)