Fix: Implement GitHub security improvements
This commit is contained in:
parent
31f47bffca
commit
02c0a87c20
11 changed files with 960 additions and 1024 deletions
|
|
@ -55,7 +55,7 @@ services:
|
|||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
ports:
|
||||
- "5000:80"
|
||||
- "2000:80"
|
||||
- "3000:3000"
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
|
|
|
|||
1767
servers/nextjs/package-lock.json
generated
1767
servers/nextjs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -66,5 +66,8 @@
|
|||
"cypress": "^14.3.3",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"overrides": {
|
||||
"brace-expansion": "2.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue