Skip to content

Commit 3fa3729

Browse files
authored
LFI vuln (v1) (#319)
* LFI vuln (v1)
1 parent 1bbcd3c commit 3fa3729

File tree

13 files changed

+384
-12
lines changed

13 files changed

+384
-12
lines changed

deploy/docker/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ services:
130130
- TLS_ENABLED=${TLS_ENABLED:-false}
131131
- TLS_CERTIFICATE=certs/server.crt
132132
- TLS_KEY=certs/server.key
133+
- FILES_LIMIT=1000
133134
depends_on:
134135
postgresdb:
135136
condition: service_healthy

deploy/helm/templates/workshop/config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ data:
2222
SERVER_PORT: {{ .Values.workshop.port | quote }}
2323
API_GATEWAY_URL: {{ if .Values.apiGatewayServiceInstall }}"https://{{ .Values.apiGatewayService.service.name }}"{{ else }}{{ .Values.apiGatewayServiceUrl }}{{ end }}
2424
TLS_ENABLED: {{ .Values.tlsEnabled | quote }}
25+
FILES_LIMIT: {{ .Values.workshop.config.filesLimit }}

deploy/helm/values.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ workshop:
126126
postgresDbDriver: postgres
127127
mongoDbDriver: mongodb
128128
secretKey: crapi
129+
filesLimit: 1000
129130
deploymentLabels:
130131
app: crapi-workshop
131132
podLabels:

services/web/src/components/serviceReport/serviceReport.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,7 @@
1414
*/
1515

1616
import React from "react";
17-
import {
18-
Card,
19-
Row,
20-
Col,
21-
Descriptions,
22-
Spin,
23-
Layout,
24-
Timeline,
25-
Typography,
26-
} from "antd";
17+
import { Card, Descriptions, Spin, Layout, Timeline, Typography } from "antd";
2718
import { PageHeader } from "@ant-design/pro-components";
2819
import { Content } from "antd/es/layout/layout";
2920
import {
@@ -33,6 +24,7 @@ import {
3324
ToolOutlined,
3425
CommentOutlined,
3526
CalendarOutlined,
27+
DownloadOutlined,
3628
} from "@ant-design/icons";
3729
import "./styles.css";
3830

@@ -65,6 +57,7 @@ interface Service {
6557
comment: string;
6658
created_on: string;
6759
}[];
60+
downloadUrl?: string;
6861
}
6962

7063
interface ServiceReportProps {
@@ -126,6 +119,18 @@ const ServiceReport: React.FC<ServiceReportProps> = ({ service }) => {
126119
</Text>
127120
</div>
128121
}
122+
extra={[
123+
<a
124+
key="1"
125+
className="download-report-button"
126+
href={service.downloadUrl}
127+
target="_blank"
128+
rel="noopener noreferrer"
129+
>
130+
<DownloadOutlined />
131+
Download Report
132+
</a>,
133+
]}
129134
/>
130135
</div>
131136

services/web/src/components/serviceReport/styles.css

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,34 @@
219219
gap: var(--spacing-sm);
220220
}
221221

222+
/* Download Report button */
223+
.download-report-button {
224+
display: inline-flex;
225+
align-items: center;
226+
gap: var(--spacing-xs);
227+
background: linear-gradient(135deg, #8b5cf6 0%, #a855f7 100%);
228+
color: white;
229+
text-decoration: none;
230+
padding: var(--spacing-sm) var(--spacing-md);
231+
border-radius: var(--border-radius-lg);
232+
font-size: 14px;
233+
font-weight: 600;
234+
transition: all 0.3s ease;
235+
border: none;
236+
cursor: pointer;
237+
box-shadow: 0 4px 12px rgba(139, 92, 246, 0.3);
238+
width: 100%;
239+
justify-content: center;
240+
}
241+
242+
.download-report-button:hover, .download-report-button:focus {
243+
background: linear-gradient(135deg, #7c3aed 0%, #9333ea 100%);
244+
transform: translateY(-2px);
245+
box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4);
246+
color: white;
247+
text-decoration: none;
248+
}
249+
222250
/* Loading State */
223251
.loading-container {
224252
display: flex;
@@ -251,6 +279,11 @@
251279
padding: var(--spacing-md);
252280
}
253281

282+
.download-report-button {
283+
padding: var(--spacing-md);
284+
font-size: 13px;
285+
}
286+
254287
.ant-descriptions-item-label {
255288
font-size: 12px;
256289
}

services/web/src/constants/APIConstant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export const requestURLS: RequestURLSType = {
6565
UPDATE_SERVICE_REQUEST_STATUS: "api/mechanic/service_request/<serviceId>",
6666
GET_VEHICLE_SERVICES: "api/merchant/service_requests/<vehicleVIN>",
6767
GET_SERVICE_REPORT: "api/mechanic/mechanic_report",
68+
DOWNLOAD_SERVICE_REPORT: "api/mechanic/download_report",
6869
BUY_PRODUCT: "api/shop/orders",
6970
GET_ORDERS: "api/shop/orders/all",
7071
GET_ORDER_BY_ID: "api/shop/orders/<orderId>",

services/web/src/containers/serviceReport/serviceReport.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ interface Service {
5252
comment: string;
5353
created_on: string;
5454
}[];
55+
downloadUrl?: string;
5556
}
5657

5758
const mapStateToProps = (state: RootState) => ({

services/web/src/sagas/vehicleSaga.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,12 @@ export function* getServiceReport(action: MyAction): Generator<any, void, any> {
377377
throw responseJSON;
378378
}
379379

380+
const filename = `report_${reportId}`;
381+
responseJSON.downloadUrl =
382+
APIService.WORKSHOP_SERVICE +
383+
requestURLS.DOWNLOAD_SERVICE_REPORT +
384+
"?filename=" +
385+
filename;
380386
yield put({ type: actionTypes.FETCHED_DATA, payload: responseJSON });
381387
callback(responseTypes.SUCCESS, responseJSON);
382388
} catch (e) {

services/workshop/crapi/mechanic/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@
4141
r"service_request$",
4242
mechanic_views.MechanicServiceRequestsView.as_view(),
4343
),
44+
re_path(r"download_report$", mechanic_views.DownloadReportView.as_view()),
4445
re_path(r"$", mechanic_views.MechanicView.as_view()),
4546
]

services/workshop/crapi/mechanic/views.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,20 @@
1515
"""
1616
contains all the views related to Mechanic
1717
"""
18+
import os
1819
import bcrypt
20+
import re
21+
from urllib.parse import unquote
22+
from django.template.loader import get_template
23+
from xhtml2pdf import pisa
1924
from django.utils import timezone
2025
from django.views.decorators.csrf import csrf_exempt
2126
from django.urls import reverse
2227
from rest_framework import status
2328
from rest_framework.response import Response
2429
from rest_framework.views import APIView
2530
from django.db import models
31+
from django.http import FileResponse
2632
from crapi_site import settings
2733
from utils.jwt import jwt_auth_required
2834
from utils import messages
@@ -40,7 +46,6 @@
4046
)
4147
from rest_framework.pagination import LimitOffsetPagination
4248

43-
4449
class SignUpView(APIView):
4550
"""
4651
Used to add a new mechanic
@@ -235,6 +240,7 @@ def get(self, request, user=None):
235240
)
236241
serializer = MechanicServiceRequestSerializer(service_request)
237242
response_data = dict(serializer.data)
243+
service_report_pdf(response_data, report_id)
238244
return Response(response_data, status=status.HTTP_200_OK)
239245

240246

@@ -366,3 +372,89 @@ def get(self, request, user=None, service_request_id=None):
366372
service_request = ServiceRequest.objects.get(id=service_request_id)
367373
serializer = MechanicServiceRequestSerializer(service_request)
368374
return Response(serializer.data, status=status.HTTP_200_OK)
375+
376+
377+
class DownloadReportView(APIView):
378+
"""
379+
A view to download a service report.
380+
"""
381+
def get(self, request, format=None):
382+
filename_from_user = request.query_params.get('filename')
383+
if not filename_from_user:
384+
return Response(
385+
{"message": "Parameter 'filename' is required."},
386+
status=status.HTTP_400_BAD_REQUEST
387+
)
388+
#Checks if input before decoding contains only allowed characters
389+
if not validate_filename(filename_from_user):
390+
return Response(
391+
{"message": "Invalid input."},
392+
status=status.HTTP_400_BAD_REQUEST
393+
)
394+
395+
filename_from_user = unquote(filename_from_user)
396+
full_path = os.path.abspath(os.path.join(settings.BASE_DIR, "reports", filename_from_user))
397+
if os.path.exists(full_path) and os.path.isfile(full_path):
398+
return FileResponse(open(full_path, 'rb'))
399+
elif not os.path.exists(full_path):
400+
return Response(
401+
{"message": f"File not found at '{full_path}'."},
402+
status=status.HTTP_404_NOT_FOUND
403+
)
404+
else:
405+
return Response(
406+
{"message": f"'{full_path}' is not a file."},
407+
status=status.HTTP_403_FORBIDDEN
408+
)
409+
410+
def validate_filename(input: str) -> bool:
411+
"""
412+
Allowed: alphanumerics, _, :, %HH
413+
"""
414+
url_encoded_pattern = re.compile(r'^(?:[A-Za-z0-9:_]|%[0-9A-Fa-f]{2})*$')
415+
return bool(url_encoded_pattern.fullmatch(input))
416+
417+
418+
def service_report_pdf(response_data, report_id):
419+
"""
420+
Generates service report's PDF file from a template and saves it to the disk.
421+
"""
422+
reports_dir = os.path.join(settings.BASE_DIR, 'reports')
423+
os.makedirs(reports_dir, exist_ok=True)
424+
report_filepath = os.path.join(reports_dir, f"report_{report_id}")
425+
426+
template = get_template('service_report.html')
427+
html_string = template.render({'service': response_data})
428+
with open(report_filepath, "w+b") as pdf_file:
429+
pisa.CreatePDF(src=html_string, dest=pdf_file)
430+
431+
manage_reports_directory()
432+
433+
434+
def manage_reports_directory():
435+
"""
436+
Checks reports directory and deletes the oldest one if the
437+
count exceeds the maximum limit.
438+
"""
439+
try:
440+
reports_dir = os.path.join(settings.BASE_DIR, 'reports')
441+
report_files = os.listdir(reports_dir)
442+
443+
if len(report_files) >= settings.FILES_LIMIT:
444+
oldest_file = None
445+
oldest_time = float('inf')
446+
for filename in report_files:
447+
filepath = os.path.join(reports_dir, filename)
448+
try:
449+
current_mtime = os.path.getmtime(filepath)
450+
if current_mtime < oldest_time:
451+
oldest_time = current_mtime
452+
oldest_file = filepath
453+
except FileNotFoundError:
454+
continue
455+
456+
if oldest_file:
457+
os.remove(oldest_file)
458+
459+
except (OSError, FileNotFoundError) as e:
460+
print(f"Error during report directory management: {e}")

0 commit comments

Comments
 (0)