mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: resolve 14 security, correctness, and robustness issues found during QA sweep
Security fixes:
- Add auth + ownership check to thumbnail endpoint (was unauthenticated)
- Validate ExifTool fieldsToRemove against safe tag name pattern
- Add SVG sanitization to pipeline execute and batch endpoints
- Replace basename() with sanitizeFilename() in 16 tool routes
- Escape SQL LIKE wildcards in file search to prevent pattern injection
- Improve settings HTML tag validation pattern
Bug fixes:
- Skip autoOrient for SVG inputs in pipeline (prevents misinterpretation)
- Remove double-encode in compress targetSize (was degrading quality)
- Fix bg-effects alpha value from 255 to 1.0 (Sharp expects float)
- Guard download stream error handler against headers-already-sent race
- Use O_EXCL atomic file creation for install lock (fixes TOCTOU race)
- Truncate collage file array to template image count
UX fixes:
- Accept empty JSON bodies on POST endpoints (install/uninstall)
- Custom JSON content type parser that treats empty body as {}
This commit is contained in:
@@ -132,7 +132,7 @@ export async function compositeOnColor(subjectBuffer: Buffer, hexColor: string):
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
channels: 4,
|
||||
background: { r, g, b, alpha: 255 },
|
||||
background: { r, g, b, alpha: 1 },
|
||||
},
|
||||
})
|
||||
.composite([{ input: subjectBuffer, blend: "over" }])
|
||||
@@ -223,7 +223,7 @@ export async function applyEffects(
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
background = await sharp({
|
||||
create: { width, height, channels: 4, background: { r, g, b, alpha: 255 } },
|
||||
create: { width, height, channels: 4, background: { r, g, b, alpha: 1 } },
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
@@ -234,10 +234,12 @@ export function buildTagArgs(settings: EditMetadataSettings): string[] {
|
||||
if (settings.iptcState) args.push(`-IPTC:Province-State=${settings.iptcState}`);
|
||||
if (settings.iptcCountry) args.push(`-IPTC:Country-PrimaryLocationName=${settings.iptcCountry}`);
|
||||
|
||||
// Field removal
|
||||
// Field removal -- only allow safe EXIF/IPTC/XMP tag names (alphanumeric, colon, hyphen)
|
||||
if (settings.fieldsToRemove && settings.fieldsToRemove.length > 0) {
|
||||
for (const field of settings.fieldsToRemove) {
|
||||
args.push(`-${field}=`);
|
||||
if (/^[A-Za-z0-9:_-]+$/.test(field)) {
|
||||
args.push(`-${field}=`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
constants,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
@@ -133,16 +135,17 @@ interface LockData {
|
||||
}
|
||||
|
||||
export function acquireInstallLock(bundleId: string): boolean {
|
||||
if (existsSync(LOCK_PATH)) {
|
||||
try {
|
||||
const fd = openSync(LOCK_PATH, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL);
|
||||
const lock: LockData = {
|
||||
bundleId,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(fd, JSON.stringify(lock, null, 2), "utf-8");
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lock: LockData = {
|
||||
bundleId,
|
||||
startedAt: new Date().toISOString(),
|
||||
};
|
||||
writeFileSync(LOCK_PATH, JSON.stringify(lock, null, 2), "utf-8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function releaseInstallLock(): void {
|
||||
|
||||
Reference in New Issue
Block a user