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
9 changes: 7 additions & 2 deletions common/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from .ai_review.dto import LlmReviewPromptDTO
from .event_log import UserEventModel # noqa
from .emails.models import Email # noqa
from .utils import is_teacher
from .utils import has_unsafe_filename, is_teacher


def current_semester() -> Optional["Semester"]:
Expand Down Expand Up @@ -316,7 +316,12 @@ def all_sources(self) -> List[SourcePath]:
for root, dirs, files in os.walk(self.dir()):
for f in files:
path = os.path.join(root, f)
sources.append(SourcePath(path[offset:], path))
virt = path[offset:]
# Skip files with XSS-dangerous characters in their names.
# These can be created by student code during Docker execution.
if has_unsafe_filename(virt):
continue
sources.append(SourcePath(virt, path))

return sources

Expand Down
2 changes: 2 additions & 0 deletions common/upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from django.core.files.uploadedfile import UploadedFile

from common.models import Submit
from common.utils import has_unsafe_filename

mimedetector = magic.Magic(mime=True)

Expand Down Expand Up @@ -207,6 +208,7 @@ def reset_file() -> UploadedFile:
try:
files = uploader.get_files()
files = [(os.path.normpath(path), f) for (path, f) in files]
files = [(path, f) for (path, f) in files if not has_unsafe_filename(path)]
files = filter_files_by_filename(files)

for path, file in files:
Expand Down
18 changes: 18 additions & 0 deletions common/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,30 @@
from typing import NewType

import django.contrib.auth.models
import nh3
import requests
from django.http import HttpRequest
from ipware import get_client_ip

from .inbus import inbus

# Characters that are dangerous in HTML context and have no legitimate use in source filenames.
_UNSAFE_FILENAME_CHARS = re.compile(r'[<>"\'&;`|]')


def has_unsafe_filename(path: str) -> bool:
"""
Returns True if any component of the path contains characters that could
be used for XSS or shell injection attacks.
Legitimate source files should never contain these characters.

Uses two layers of detection:
1. Regex for shell/HTML dangerous characters
2. nh3.is_html() to catch any HTML syntax patterns the regex might miss
"""
return bool(_UNSAFE_FILENAME_CHARS.search(path)) or nh3.is_html(path)


IPAddressString = NewType("IPAddressString", str)


Expand Down
5 changes: 0 additions & 5 deletions evaluator/images/gcc/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1 @@
FROM kelvin/base
ADD wrapper /wrapper/gcc
ADD wrapper /wrapper/cc
ADD wrapper /wrapper/g++
ADD entry.py /
CMD /entry.py
139 changes: 0 additions & 139 deletions evaluator/images/gcc/entry.py

This file was deleted.

2 changes: 0 additions & 2 deletions evaluator/images/gcc/wrapper

This file was deleted.

81 changes: 80 additions & 1 deletion evaluator/pipelines.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from . import testsets

from .utils import parse_human_size, copyfile
from . import type_handlers
from dataclasses import fields, replace

logger = logging.getLogger("evaluator")

Expand Down Expand Up @@ -40,6 +42,83 @@
}


class TypePipe:
handler_cls = None
id = None
default_limits = type_handlers.ExecutionLimits(fsize="16M", memory="128M", network="none")

def __init__(self, image=None, limits=None, before=None, **kwargs):
self.image = image
self.kwargs = kwargs
self.limits = limits if limits else {}
self.before = [] if not before else before

def _resolve_limits(self):
# 1. Start with class defaults (e.g. GccPipe defaults)
limits_obj = self.default_limits

# 2. Update with user config limits by iterating over dataclass fields
updates = {}
for f in fields(type_handlers.ExecutionLimits):
if f.name in self.limits:
val = self.limits[f.name]
if val is None:
continue
# Coerce val to the field's type if needed (e.g. "30" → int).
# isinstance(field_type, type) skips complex hints like Optional[str].
field_type = f.type
try:
if isinstance(field_type, type) and not isinstance(val, field_type):
val = field_type(val)
updates[f.name] = val
except (TypeError, ValueError):
logger.warning(
"Could not coerce limit %r value %r to %s, using default",
f.name,
val,
field_type.__name__,
)

return replace(limits_obj, **updates)

def run(self, evaluation):
result_dir = os.path.join(evaluation.result_path, self.id)
os.mkdir(result_dir)

image_name = self.image
if self.image:
image_name = prepare_container(docker_image(self.image), self.before)

resolved_limits = self._resolve_limits()

handler = self.handler_cls(self.kwargs, evaluation, resolved_limits)
result = handler.compile(image_name)

if result.comments or result.tests:
with open(os.path.join(result_dir, "piperesult.json"), "w") as f:
json.dump({"comments": result.simple_comments, "tests": result.tests}, f, indent=4)

with open(os.path.join(result_dir, "result.html"), "w") as f:
f.write(result.html)

return {
"failed": not result.success,
"html": result.html,
"comments": result.simple_comments,
"tests": result.tests,
}


class GccPipe(TypePipe):
handler_cls = type_handlers.Gcc
default_limits = type_handlers.ExecutionLimits(fsize="64M", memory="128M", network="none")

def __init__(self, **kwargs):
if "image" not in kwargs:
kwargs["image"] = "kelvin/gcc"
super().__init__(**kwargs)


def create_docker_cmd(evaluation, image, additional_args=None, cmd=None, limits=None, env=None):
if not limits:
limits = {}
Expand Down Expand Up @@ -86,7 +165,7 @@ def fmt_value(v):
"-v",
evaluation.submit_path + ":/work",
"--ulimit",
f'fsize={limits["fsize"]}:{limits["fsize"]}',
f"fsize={limits['fsize']}:{limits['fsize']}",
"-m",
str(limits["memory"]),
"--memory-swap",
Expand Down
Loading
Loading