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
2 changes: 1 addition & 1 deletion introduction/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
class NewUserForm(UserCreationForm):
email = forms.EmailField(required=True)

class Meta:
class Meta(UserCreationForm.Meta):
model = User
fields = ("username", "email", "password1", "password2")

Expand Down
11 changes: 9 additions & 2 deletions introduction/lab_code/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,15 @@

import yaml

stream = open('/home/fox/test.yaml', 'r')
data = yaml.load(stream)
try:
with open('/home/fox/test.yaml', 'r') as stream:
data = yaml.safe_load(stream)
except yaml.YAMLError as e:
print(f"Error parsing YAML: {e}")
exit(1)
except IOError as e:
print(f"Error reading file: {e}")
exit(1)

'''
stdout, stderr = data.communicate()
Expand Down
21 changes: 16 additions & 5 deletions introduction/mitre.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,8 +240,19 @@ def mitre_lab_17_api(request):
ip = request.POST.get('ip')
command = "nmap " + ip
res, err = command_out(command)
res = res.decode()
err = err.decode()
pattern = "STATE SERVICE.*\\n\\n"
ports = re.findall(pattern, res,re.DOTALL)[0][14:-2].split('\n')
return JsonResponse({'raw_res': str(res), 'raw_err': str(err), 'ports': ports})

# Add type checking and error handling for decoding
try:
if isinstance(res, bytes):
res = res.decode()
if isinstance(err, bytes):
err = err.decode()
except UnicodeDecodeError as e:
return JsonResponse({'error': 'Failed to decode command output'}, status=500)

try:
pattern = "STATE SERVICE.*\\n\\n"
ports = re.findall(pattern, res, re.DOTALL)[0][14:-2].split('\n')
return JsonResponse({'raw_res': str(res), 'raw_err': str(err), 'ports': ports})
except (IndexError, AttributeError) as e:
return JsonResponse({'error': 'Failed to parse command output'}, status=500)
22 changes: 15 additions & 7 deletions introduction/playground/ssrf/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import os
from pathlib import Path


def ssrf_lab(file):
try:
dirname = os.path.dirname(__file__)
filename = os.path.join(dirname, file)
file = open(filename,"r")
data = file.read()
return {"blog":data}
except:
return {"blog": "No blog found"}
base_dir = Path(__file__).parent
file_path = base_dir / file

# Ensure the file path doesn't escape the base directory
if not str(file_path.resolve()).startswith(str(base_dir.resolve())):
return {"blog": "Access denied: Invalid path"}

with open(file_path, "r") as f:
data = f.read()
return {"blog": data}
except FileNotFoundError:
return {"blog": "No blog found"}
except (PermissionError, OSError) as e:
return {"blog": "Error accessing file"}
2 changes: 1 addition & 1 deletion introduction/static/js/a9.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ event3 = function(){
document.getElementById("a9_d3").style.display = 'flex';
for (var i = 0; i < data.logs.length; i++) {
var li = document.createElement("li");
li.innerHTML = data.logs[i];
li.textContent = data.logs[i]; // Using textContent for secure text rendering
document.getElementById("a9_d3").appendChild(li);
}
})
Expand Down
84 changes: 66 additions & 18 deletions introduction/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,38 @@ def xxe_see(request):

@csrf_exempt
def xxe_parse(request):

parser = make_parser()
parser.setFeature(feature_external_ges, True)
doc = parseString(request.body.decode('utf-8'), parser=parser)
for event, node in doc:
if event == START_ELEMENT and node.tagName == 'text':
doc.expandNode(node)
text = node.toxml()
startInd = text.find('>')
endInd = text.find('<', startInd)
text = text[startInd + 1:endInd:]
p=comments.objects.filter(id=1).update(comment=text)

return render(request, 'Lab/XXE/xxe_lab.html')
try:
# Use defusedxml's parseString which has XXE protections enabled by default
from defusedxml.pulldom import parseString

# Validate content type
if not request.content_type == 'application/xml':
return JsonResponse({'error': 'Invalid content type. Expected application/xml'}, status=400)

# Add size limit check - 10KB should be more than enough for this example
if len(request.body) > 10240: # 10KB limit
return JsonResponse({'error': 'XML content too large'}, status=400)

doc = parseString(request.body.decode('utf-8'))
text = None

for event, node in doc:
if event == START_ELEMENT and node.tagName == 'text':
doc.expandNode(node)
text = node.toxml()

if text is None:
return JsonResponse({'error': 'Missing text element in XML'}, status=400)

startInd = text.find('>')
endInd = text.find('<', startInd)
text = text[startInd + 1:endInd:]

p = comments.objects.filter(id=1).update(comment=text)
return render(request, 'Lab/XXE/xxe_lab.html')

except Exception as e:
return JsonResponse({'error': 'XML parsing error'}, status=400)

def auth_home(request):
return render(request,'Lab/AUTH/auth_home.html')
Expand Down Expand Up @@ -952,11 +970,41 @@ def ssrf_lab2(request):

elif request.method == "POST":
url = request.POST["url"]

# Basic SSRF protection - only allow http/https schemes
if not url.startswith(('http://', 'https://')):
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": "Invalid URL scheme. Only HTTP(S) is allowed."})

try:
response = requests.get(url)
return render(request, "Lab/ssrf/ssrf_lab2.html", {"response": response.content.decode()})
except:
return render(request, "Lab/ssrf/ssrf_lab2.html", {"error": "Invalid URL"})
# Add timeout and max size limit
response = requests.get(url, timeout=5, stream=True)

# Validate content type
content_type = response.headers.get('content-type', '')
if not content_type.startswith('text/'):
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": "Only text content types are supported."})

# Read with size limit (1MB)
content = response.raw.read(1024 * 1024)
if response.raw.read(1): # Check if there's more content
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": "Response too large. Maximum size is 1MB."})

# Send raw content to template without decoding
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"response": content})

except requests.exceptions.Timeout:
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": "Request timed out."})
except requests.exceptions.RequestException as e:
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": f"Failed to fetch URL: {str(e)}"})
except Exception as e:
return render(request, "Lab/ssrf/ssrf_lab2.html",
{"error": "An unexpected error occurred."})
#--------------------------------------- Server-side template injection --------------------------------------#

def ssti(request):
Expand Down
11 changes: 10 additions & 1 deletion pygoat/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True

ALLOWED_HOSTS = ['pygoat.herokuapp.com', '0.0.0.0.']
# Development vs Production configuration using environment variables
if DEBUG:
ALLOWED_HOSTS = [
'localhost',
'127.0.0.1'
]
else:
ALLOWED_HOSTS = [
'pygoat.herokuapp.com'
]


# Application definition
Expand Down