Fix: Implement GitHub security improvements

This commit is contained in:
shiva raj badu 2025-07-11 21:25:35 +05:45
parent 31f47bffca
commit 02c0a87c20
11 changed files with 960 additions and 1024 deletions

View file

@ -55,7 +55,7 @@ services:
context: .
dockerfile: Dockerfile.dev
ports:
- "5000:80"
- "2000:80"
- "3000:3000"
- "8000:8000"
volumes:

View file

@ -143,9 +143,9 @@ class GeneratePresentationHandler(FetchAssetsOnPresentationGenerationMixin):
async with session.post(
f"http://localhost/api/slide-metadata",
json={
"url": f"http://localhost/presentation?id={self.presentation_id}",
"theme": self.theme["name"],
"customColors": self.theme["colors"],
"id": self.presentation_id,
# "theme": self.theme["name"],
# "customColors": self.theme["colors"],
},
) as response:
export_request_body = await response.json()

View file

@ -37,11 +37,12 @@ const SlideFooter: React.FC = () => {
const whiteLogoRef = useRef<HTMLInputElement | null>(null);
const darkLogoRef = useRef<HTMLInputElement | null>(null);
const { footerProperties, setFooterProperties, saveFooterProperties, resetFooterProperties } = useFooterContext();
const { footerProperties, setFooterProperties, saveFooterProperties, resetFooterProperties, isPropertyChanged, setIsPropertyChanged } = useFooterContext();
const handleSave = async () => {
await saveFooterProperties(footerProperties);
setIsPropertyChanged(false);
toast({
title: "Footer properties saved successfully",
});
@ -56,16 +57,53 @@ const SlideFooter: React.FC = () => {
};
const updateProperty = (path: string, value: any): void => {
setIsPropertyChanged(true);
const keys = path.split(".");
// Security: Validate path to prevent prototype pollution
const dangerousKeys = ['__proto__', 'constructor', 'prototype'];
if (keys.some(key => dangerousKeys.includes(key))) {
console.warn('Attempted prototype pollution with path:', path);
return;
}
const allowedPaths = [
'logoProperties.showLogo',
'logoProperties.logoPosition',
'logoProperties.opacity',
'logoProperties.logoImage.light',
'logoProperties.logoImage.dark',
'logoScale',
'logoOffset.x',
'logoOffset.y',
'footerMessage.showMessage',
'footerMessage.message',
'footerMessage.fontSize',
'footerMessage.opacity'
]
if (!allowedPaths.includes(path)) {
console.error(`Invalid path: ${path}`);
return;
}
setFooterProperties((prevProps: FooterProperties) => {
const newProps = { ...prevProps };
let current: any = newProps;
for (let i = 0; i < keys.length - 1; i++) {
if (dangerousKeys.includes(keys[i])) {
console.warn('Attempted prototype pollution with path:', path);
return prevProps;
}
current[keys[i]] = { ...current[keys[i]] };
current = current[keys[i]];
}
const finalKey = keys[keys.length - 1];
if (dangerousKeys.includes(finalKey)) {
console.warn('Dangerous final key detected:', finalKey);
return prevProps;
}
current[keys[keys.length - 1]] = value;
return newProps;
});
@ -143,6 +181,7 @@ const SlideFooter: React.FC = () => {
const handleWhiteLogoUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setIsPropertyChanged(true);
const file = event.target.files?.[0];
if (!file) return;
@ -181,6 +220,7 @@ const SlideFooter: React.FC = () => {
const handleDarkLogoUpload = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setIsPropertyChanged(true);
const file = event.target.files?.[0];
if (!file) return;
@ -236,7 +276,14 @@ const SlideFooter: React.FC = () => {
};
const handleSheetClose = () => {
handleSave();
if (isPropertyChanged) {
toast({
title: "Unsaved Changes",
description: "Please save changes before closing the editor",
variant: "destructive",
});
return;
}
setShowEditor(false);
};

View file

@ -35,6 +35,8 @@ interface FooterContextProps {
setFooterProperties: (newProperties: FooterProperties | ((prev: FooterProperties) => FooterProperties)) => void;
resetFooterProperties: () => Promise<void>;
saveFooterProperties: (newProperties: FooterProperties) => Promise<void>;
isPropertyChanged: boolean;
setIsPropertyChanged: (newIsPropertyChanged: boolean) => void;
}
const FooterContext = createContext<FooterContextProps | undefined>(undefined);
@ -52,6 +54,7 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
}) => {
const [footerProperties, setFooterProperties] = useState<FooterProperties>(defaultFooterProperties);
const footerService = useFooterService();
const [isPropertyChanged, setIsPropertyChanged] = useState(false);
// Load footer properties only once when the provider mounts
useEffect(() => {
@ -98,6 +101,8 @@ export const FooterProvider: React.FC<{ children: React.ReactNode }> = ({
setFooterProperties,
resetFooterProperties,
saveFooterProperties,
isPropertyChanged,
setIsPropertyChanged,
}}
>
{children}

View file

@ -132,9 +132,7 @@ const Header = ({
const metadata = await (await fetch('/api/slide-metadata', {
method: 'POST',
body: JSON.stringify({
url: 'http://localhost/presentation?id=' + presentation_id,
theme: currentTheme,
customColors: currentColors,
id: presentation_id,
})
})).json()
@ -207,7 +205,7 @@ const Header = ({
const response = await fetch('/api/export-as-pdf', {
method: 'POST',
body: JSON.stringify({
url: `http://localhost/pdf-maker?id=${presentation_id}`,
id: presentation_id,
title: presentationData!.presentation!.title,
})
});

View file

@ -185,45 +185,54 @@ const SupportingDoc = ({ files, onFilesChange }: SupportingDocProps) => {
</h3>
</div>
<div data-testid="file-list" className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
{filesWithIds.map((file) => (
<div key={file.id}
className="bg-white rounded-lg border border-gray-200 overflow-hidden
{filesWithIds.map((file) => {
const isImage = isImageFile(file);
let imageUrl = '';
if (isImage) {
imageUrl = URL.createObjectURL(file);
}
return (
(
<div key={file.id}
className="bg-white rounded-lg border border-gray-200 overflow-hidden
hover:border-purple-200 group relative"
>
<div className="p-4 bg-purple-50 group-hover:bg-purple-100
>
<div className="p-4 bg-purple-50 group-hover:bg-purple-100
transition-colors flex items-center justify-center relative"
>
{isImageFile(file) ? (
<img src={URL.createObjectURL(file)} className="w-10 h-10 text-purple-600" />
) : (
<File className="w-8 h-8 text-purple-600" />
)}
<button
onClick={(e) => {
e.stopPropagation()
removeFile(file.id)
}}
className="absolute top-1 right-2 p-1.5
>
{isImage ? (
<img src={imageUrl} className="w-10 h-10 text-purple-600" />
) : (
<File className="w-8 h-8 text-purple-600" />
)}
<button
onClick={(e) => {
e.stopPropagation()
removeFile(file.id)
}}
className="absolute top-1 right-2 p-1.5
bg-white/80 backdrop-blur-sm rounded-full
text-gray-500 hover:text-red-500
shadow-sm hover:shadow-md
transition-all duration-200"
aria-label="Remove file"
>
<X className="w-4 h-4" />
</button>
</div>
aria-label="Remove file"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="p-3 relative">
<p className="text-sm font-medium text-gray-700 truncate mb-1 pr-2">
{file.name || 'Unnamed File'}
</p>
<p className="text-xs text-gray-500">
{formatFileSize(file.size)}
</p>
</div>
</div>
))}
<div className="p-3 relative">
<p className="text-sm font-medium text-gray-700 truncate mb-1 pr-2">
{file.name || 'Unnamed File'}
</p>
<p className="text-xs text-gray-500">
{formatFileSize(file.size)}
</p>
</div>
</div>
)
)
})}
</div>
</div>
</div>

View file

@ -7,14 +7,16 @@ import { NextResponse, NextRequest } from 'next/server';
export async function POST(req: NextRequest) {
const { url, title } = await req.json();
const { id, title } = await req.json();
if (!id) {
return NextResponse.json({ error: "Missing Presentation ID" }, { status: 400 });
}
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle0' });
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0' });
const pdfBuffer = await page.pdf({
printBackground: true,

View file

@ -6,7 +6,25 @@ export async function POST(request: Request) {
try {
const { filePath } = await request.json();
const normalizedPath = path.normalize(filePath);
const content= fs.readFileSync(normalizedPath, 'utf-8');
const allowedBaseDirs = [
process.env.APP_DATA_DIRECTORY || '/app/user_data',
process.env.TEMP_DIRECTORY || '/tmp',
'/app/user_data'
];
const resolvedPath = path.resolve(normalizedPath);
const isPathAllowed = allowedBaseDirs.some(baseDir => {
const resolvedBaseDir = path.resolve(baseDir);
return resolvedPath.startsWith(resolvedBaseDir + path.sep) || resolvedPath === resolvedBaseDir;
});
if (!isPathAllowed) {
console.error('Unauthorized file access attempt:', resolvedPath);
return NextResponse.json(
{ error: 'Access denied: File path not allowed' },
{ status: 403 }
);
}
const content= fs.readFileSync(resolvedPath, 'utf-8');
return NextResponse.json({ content });
} catch (error) {

View file

@ -87,27 +87,15 @@ interface SlideMetadata {
elements: SlideElement[];
}
interface ThemeParams {
theme: string;
customColors?: {
slideBg: string;
slideTitle: string;
slideHeading: string;
slideDescription: string;
slideBox: string;
};
}
export async function POST(request: NextRequest) {
let browser;
try {
const body = await request.json();
const { url, theme, customColors } = body;
if (!url) {
return NextResponse.json({ error: "Missing URL" }, { status: 400 });
const { id } = body;
if (!id) {
return NextResponse.json({ error: "Missing Presentation ID" }, { status: 400 });
}
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
@ -117,13 +105,13 @@ export async function POST(request: NextRequest) {
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
try {
await page.goto(url, {
await page.goto(`http://localhost/presentation?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,
});
} catch (error) {
await browser.close();
return NextResponse.json({ error: "Failed to Navigate to provided URL" }, { status: 500 });
return NextResponse.json({ error: "Failed to navigate to provided URL" }, { status: 500 });
}
try {
@ -131,27 +119,6 @@ export async function POST(request: NextRequest) {
timeout: 60000,
});
await page.evaluate(
async (params: ThemeParams) => {
const { theme, customColors } = params;
const containers = document.querySelectorAll(".slide-theme");
containers.forEach((container) => {
container.removeAttribute("data-theme");
container.setAttribute("data-theme", theme);
});
if (theme === "custom" && customColors) {
const root = document.documentElement;
root.style.setProperty("--custom-slide-bg", customColors.slideBg);
root.style.setProperty("--custom-slide-title", customColors.slideTitle);
root.style.setProperty("--custom-slide-heading", customColors.slideHeading);
root.style.setProperty("--custom-slide-description", customColors.slideDescription);
root.style.setProperty("--custom-slide-box", customColors.slideBox);
}
},
{ theme, customColors }
);
} catch (error) {
await browser.close();
return NextResponse.json({ error: "Slide container not found" }, { status: 500 });

File diff suppressed because it is too large Load diff

View file

@ -66,5 +66,8 @@
"cypress": "^14.3.3",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
"overrides": {
"brace-expansion": "2.0.2"
}
}