From 55726bced440e6d755b40b4517ff78ddecf301be Mon Sep 17 00:00:00 2001 From: seoonju Date: Fri, 1 Aug 2025 14:30:35 +0900 Subject: [PATCH 1/3] [ AutoFiC ] Create package.json and CI workflow --- .github/workflows/pr_notify.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/workflows/pr_notify.yml diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml new file mode 100644 index 000000000..2b34036d0 --- /dev/null +++ b/.github/workflows/pr_notify.yml @@ -0,0 +1,20 @@ +name: PR Notifier + +on: + pull_request: + types: [opened, reopened, closed] + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Notify Discord + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + curl -H "Content-Type: application/json" -d '{"content": "🔔 Pull Request [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) by ${{ github.event.pull_request.user.login }} - ${{ github.event.action }}"}' $DISCORD_WEBHOOK_URL + - name: Notify Slack + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + curl -H "Content-Type: application/json" -d '{"text": ":bell: Pull Request <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> by ${{ github.event.pull_request.user.login }} - ${{ github.event.action }}"}' $SLACK_WEBHOOK_URL From 1c2ffcdb71c301fa9783243fb2db78ffccbd6ce5 Mon Sep 17 00:00:00 2001 From: seoonju Date: Fri, 1 Aug 2025 14:30:37 +0900 Subject: [PATCH 2/3] [ AutoFiC ] 4 malicious code detected!! --- src/server/services/api/api-i18n.js | 6 ++ .../services/api/api-profile-definitions.js | 71 ++++++++++++++----- src/server/services/api/api-state.js | 2 +- src/server/services/api/api-svg.js | 16 ++++- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/server/services/api/api-i18n.js b/src/server/services/api/api-i18n.js index 3b1e95ddd..9fec56c72 100644 --- a/src/server/services/api/api-i18n.js +++ b/src/server/services/api/api-i18n.js @@ -102,6 +102,12 @@ export const saveMissing = (req, res) => { const lng = req.params.lng; const ns = req.params.ns; + // Validate and sanitize the input to prevent path traversal + if (!/^[a-zA-Z0-9_-]+$/.test(lng) || !/^[a-zA-Z0-9_-]+$/.test(ns)) { + res.status(400).send('Invalid language or namespace'); + return; + } + const mergedFile = path.join(settings.assets.app.path, 'i18n', lng, `${ns}.json`); const mergedObject = JSON.parse(fs.readFileSync(mergedFile, 'utf8')); diff --git a/src/server/services/api/api-profile-definitions.js b/src/server/services/api/api-profile-definitions.js index b211b4f39..8448e9c3b 100644 --- a/src/server/services/api/api-profile-definitions.js +++ b/src/server/services/api/api-profile-definitions.js @@ -33,7 +33,12 @@ export const getRawDefinition = (req, res) => { } const filename = `${definitionId}.def.json`; - const configDir = `${DataStorage.configDir}/${configPath}/${filename}`; + const configDir = path.resolve(DataStorage.configDir, configPath, filename); + if (!configDir.startsWith(DataStorage.configDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + try { const readFileSync = fs.readFileSync(configDir); const parse = JSON.parse(readFileSync); @@ -121,8 +126,13 @@ export const createDefinition = async (req, res) => { definitionLoader.fromObject(definition); const configPath = isPublicProfile(definitionLoader.definitionId) ? headType : (req.body.configPath ?? ''); - const filePath = path.join(`${DataStorage.configDir}/${configPath}`, `${definitionLoader.definitionId}.def.json`); - const backupPath = path.join(`${DataStorage.activeConfigDir}/${configPath}`, `${definitionLoader.definitionId}.def.json`); + const filePath = path.resolve(DataStorage.configDir, configPath, `${definitionLoader.definitionId}.def.json`); + const backupPath = path.resolve(DataStorage.activeConfigDir, configPath, `${definitionLoader.definitionId}.def.json`); + if (!filePath.startsWith(DataStorage.configDir) || !backupPath.startsWith(DataStorage.activeConfigDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + const data = JSON.stringify(definitionLoader.toJSON(), null, 2); if (!fs.existsSync(backupPath)) { try { @@ -171,10 +181,15 @@ export const updateDefaultDefinition = (req, res) => { let filePath = ''; if (isPublicProfile(definitionId)) { - filePath = path.join(`${DataStorage.defaultConfigDir}/${headType}`, `${definitionId}.def.json`); + filePath = path.resolve(DataStorage.defaultConfigDir, headType, `${definitionId}.def.json`); } else { - filePath = path.join(`${DataStorage.defaultConfigDir}/${configPath}`, `${definitionId}.def.json`); + filePath = path.resolve(DataStorage.defaultConfigDir, configPath, `${definitionId}.def.json`); + } + if (!filePath.startsWith(DataStorage.defaultConfigDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; } + const data = JSON.stringify(definitionLoader.toJSON(), null, 2); fs.writeFile(filePath, data, 'utf8', (err) => { if (err) { @@ -194,7 +209,12 @@ export const createTmpDefinition = (req, res) => { definitionLoader.fromObject(definition); const uploadName = `${filename ?? definitionLoader.definitionId}.def.json`; - const filePath = path.join(`${DataStorage.tmpDir}`, uploadName); + const filePath = path.resolve(DataStorage.tmpDir, uploadName); + if (!filePath.startsWith(DataStorage.tmpDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + fs.writeFile(filePath, JSON.stringify(definitionLoader.toJSON(), null, 2), 'utf8', (err) => { if (err) { log.error(err); @@ -212,8 +232,13 @@ export const removeDefinition = (req, res) => { const { definitionId } = req.params; const configPath = req.body.configPath; - const filePath = path.join(`${DataStorage.configDir}/${configPath}`, `${definitionId}.def.json`); - const backupPath = path.join(`${DataStorage.activeConfigDir}/${configPath}`, `${definitionId}.def.json`); + const filePath = path.resolve(DataStorage.configDir, configPath, `${definitionId}.def.json`); + const backupPath = path.resolve(DataStorage.activeConfigDir, configPath, `${definitionId}.def.json`); + if (!filePath.startsWith(DataStorage.configDir) || !backupPath.startsWith(DataStorage.activeConfigDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + fs.unlink(filePath, (err) => { if (err) { log.error(err); @@ -270,12 +295,17 @@ export const updateDefinition = async (req, res) => { let filePath = ''; let activeRecoverPath = ''; if (isPublicProfile(definitionId)) { - filePath = path.join(`${DataStorage.configDir}/${headType}`, `${definitionId}.def.json`); - activeRecoverPath = path.join(`${DataStorage.activeConfigDir}/${headType}`, `${definitionId}.def.json`); + filePath = path.resolve(DataStorage.configDir, headType, `${definitionId}.def.json`); + activeRecoverPath = path.resolve(DataStorage.activeConfigDir, headType, `${definitionId}.def.json`); } else { - filePath = path.join(`${DataStorage.configDir}/${configPath}`, `${definitionId}.def.json`); - activeRecoverPath = path.join(`${DataStorage.activeConfigDir}/${configPath}`, `${definitionId}.def.json`); + filePath = path.resolve(DataStorage.configDir, configPath, `${definitionId}.def.json`); + activeRecoverPath = path.resolve(DataStorage.activeConfigDir, configPath, `${definitionId}.def.json`); } + if (!filePath.startsWith(DataStorage.configDir) || !activeRecoverPath.startsWith(DataStorage.activeConfigDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + if (!fs.existsSync(DataStorage.activeConfigDir)) { try { await fs.copy(DataStorage.configDir, DataStorage.activeConfigDir); @@ -308,7 +338,7 @@ const isSourceFormDefault = (obj) => { export const uploadDefinition = (req, res) => { const { headType } = req.params; const { definitionId, uploadName, configPath } = req.body; - const readFileSync = fs.readFileSync(`${DataStorage.tmpDir}/${uploadName}`, 'utf-8'); + const readFileSync = fs.readFileSync(path.resolve(DataStorage.tmpDir, uploadName), 'utf-8'); let obj; try { obj = JSON.parse(readFileSync); @@ -322,7 +352,7 @@ export const uploadDefinition = (req, res) => { obj = {}; } - if (!obj.inherits || !fs.existsSync(`${DataStorage.configDir}/${headType}/${obj.inherits}.json`)) { + if (!obj.inherits || !fs.existsSync(path.resolve(DataStorage.configDir, headType, `${obj.inherits}.json`))) { obj.inherits = 'snapmaker2'; } @@ -334,8 +364,13 @@ export const uploadDefinition = (req, res) => { const definitionLoader = new DefinitionLoader(); try { definitionLoader.loadJSON(headType, definitionId, obj); - const filePath = path.join(`${DataStorage.configDir}/${configPath}`, `${definitionId}.def.json`); - const backupPath = path.join(`${DataStorage.activeConfigDir}/${configPath}`, `${definitionId}.def.json`); + const filePath = path.resolve(DataStorage.configDir, configPath, `${definitionId}.def.json`); + const backupPath = path.resolve(DataStorage.activeConfigDir, configPath, `${definitionId}.def.json`); + if (!filePath.startsWith(DataStorage.configDir) || !backupPath.startsWith(DataStorage.activeConfigDir)) { + res.status(ERR_BAD_REQUEST).send({ err: 'Invalid path.' }); + return; + } + const data = JSON.stringify(definitionLoader.toJSON(), null, 2); const callback = () => { fsWriteFile(backupPath, data, res, (err) => { @@ -366,7 +401,7 @@ export const getParameterDoc = (req, res) => { const langDir = lang.toUpperCase() === 'ZH-CN' ? 'CN' : lang.toUpperCase(); const fileRelativePath = `${langDir}/${category}/${key}.md`; - const filePath = `${DataStorage.getParameterDocumentDir()}/${fileRelativePath}`; + const filePath = path.resolve(DataStorage.getParameterDocumentDir(), fileRelativePath); let content; if (fs.existsSync(filePath)) { @@ -374,7 +409,7 @@ export const getParameterDoc = (req, res) => { } else if (lang !== 'en') { log.info(`Request: "${fileRelativePath}"\nNo documentation was found for the user's language ${lang}. An English version was given.`); - const filePathEN = `${DataStorage.getParameterDocumentDir()}/EN/${category}/${key}.md`; + const filePathEN = path.resolve(DataStorage.getParameterDocumentDir(), 'EN', category, `${key}.md`); content = fs.readFileSync(filePathEN, 'utf-8'); } diff --git a/src/server/services/api/api-state.js b/src/server/services/api/api-state.js index c1c6db48a..7d49faea7 100644 --- a/src/server/services/api/api-state.js +++ b/src/server/services/api/api-state.js @@ -20,7 +20,7 @@ export const get = (req, res) => { } const value = config.get(key); - res.send(value); + res.json(value); // Changed from res.send(value) to res.json(value) }; export const unset = (req, res) => { diff --git a/src/server/services/api/api-svg.js b/src/server/services/api/api-svg.js index 83221eb5b..d6110a2a0 100644 --- a/src/server/services/api/api-svg.js +++ b/src/server/services/api/api-svg.js @@ -8,7 +8,7 @@ export const convertRasterToSvg = async (req, res) => { // options: { filename, vectorThreshold, invert, turdSize } const options = req.body; const result = await convertRaster(options); - res.send(result); + res.send(escapeHtml(result)); // Escape the result before sending }; export const convertTextToSvg = async (req, res) => { @@ -16,7 +16,7 @@ export const convertTextToSvg = async (req, res) => { const options = req.body; try { const result = await convertText(options); - res.send(result); + res.send(escapeHtml(result)); // Escape the result before sending } catch (e) { log.error(`Fail to convert text to SVG: ${e}`); res.status(ERR_INTERNAL_SERVER_ERROR).send({ @@ -29,5 +29,15 @@ export const convertOneLineTextToSvg = async (req, res) => { // options: { text, font, name, size, sourceWidth, sourceHeight } const options = req.body; const result = await convertOneLineText(options); - res.send(result); + res.send(escapeHtml(result)); // Escape the result before sending }; + +// Helper function to escape HTML +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} From 50f41b49c9d30871ca17874635a98e29df992310 Mon Sep 17 00:00:00 2001 From: seoonju Date: Fri, 1 Aug 2025 14:30:55 +0900 Subject: [PATCH 3/3] chore: remove CI workflow before upstream PR --- .github/workflows/pr_notify.yml | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 .github/workflows/pr_notify.yml diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml deleted file mode 100644 index 2b34036d0..000000000 --- a/.github/workflows/pr_notify.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: PR Notifier - -on: - pull_request: - types: [opened, reopened, closed] - -jobs: - notify: - runs-on: ubuntu-latest - steps: - - name: Notify Discord - env: - DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} - run: | - curl -H "Content-Type: application/json" -d '{"content": "🔔 Pull Request [${{ github.event.pull_request.title }}](${{ github.event.pull_request.html_url }}) by ${{ github.event.pull_request.user.login }} - ${{ github.event.action }}"}' $DISCORD_WEBHOOK_URL - - name: Notify Slack - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - run: | - curl -H "Content-Type: application/json" -d '{"text": ":bell: Pull Request <${{ github.event.pull_request.html_url }}|${{ github.event.pull_request.title }}> by ${{ github.event.pull_request.user.login }} - ${{ github.event.action }}"}' $SLACK_WEBHOOK_URL