Skip to content

Commit

Permalink
Untrusted inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
yoanm committed Mar 20, 2024
1 parent 455c1a1 commit 17a13b1
Show file tree
Hide file tree
Showing 19 changed files with 192 additions and 123 deletions.
9 changes: 4 additions & 5 deletions .github/actions/reports-group/codacy-uploader/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,10 @@ runs:
id: build-outputs
uses: actions/github-script@v7
env:
METADATA: ${{ steps.load-metadata.outputs.metadata }}
REPORTS: ${{ steps.build-uploader-options.outputs.coverage-reports }}
with:
script: |
core.info('Build output');
const {METADATA} = process.env;
const metadata = JSON.parse(METADATA);
core.setOutput('reports', metadata.reportPaths.split(',').join('\n'));
const {REPORTS} = process.env;
core.setOutput('reports', REPORTS.split(',').join('\n'));
14 changes: 7 additions & 7 deletions .github/actions/reports-group/codecov-uploader/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,15 +98,15 @@ runs:
id: build-outputs
uses: actions/github-script@v7
env:
METADATA: ${{ steps.load-metadata.outputs.metadata }}
REPORT_NAME: ${{ steps.build-uploader-options.outputs.name }}
REPORT_FILES: ${{ steps.build-uploader-options.outputs.files }}
REPORT_FLAGS: ${{ steps.build-uploader-options.outputs.flags }}
with:
script: |
core.info('Build output');
const {METADATA} = process.env;
const {REPORT_NAME, REPORT_FILES, REPORT_FLAGS} = process.env;
const metadata = JSON.parse(METADATA);
core.setOutput('name', metadata.name);
core.setOutput('reports', metadata.reportPaths.split(',').join('\n'));
if (metadata.flags.length > 0) {
core.setOutput('flags', metadata.flags.split(',').join('\n'));
}
core.setOutput('name', REPORT_NAME);
core.setOutput('reports', REPORT_FILES.split(',').join('\n'));
core.setOutput('flags', undefined !== REPORT_FLAGS ? REPORT_FLAGS.split(',').join('\n') : '');
4 changes: 2 additions & 2 deletions .github/actions/reports-group/create/dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/actions/reports-group/create/dist/index.js.map

Large diffs are not rendered by default.

66 changes: 35 additions & 31 deletions .github/actions/reports-group/create/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,117 +4,121 @@ const fs = require('fs'); // @TODO move to 'imports from' when moved to TS !
const core = require('@actions/core'); // @TODO move to 'imports from' when moved to TS !
const io = require('@actions/io'); // @TODO move to 'imports from' when moved to TS !

const {path: pathSDK, glob: globSDK, outputs: outputsSDK, CONSTANTS: SDK_CONSTANTS} = require('./node-sdk'); // @TODO move to 'imports from' when moved to TS !
const SDK = require('./node-sdk'); // @TODO move to 'imports from' when moved to TS !

async function run() {
const trustedPathHelper = SDK.path.trustedPathHelpers();
/** INPUTS **/
const NAME_INPUT = core.getInput('NAME', {required: true});
const NAME_INPUT = core.getInput('name', {required: true});
const FORMAT_INPUT = core.getInput('format', {required: true});
const REPORTS_INPUT = core.getInput('files', {required: true});
// Following inputs are not marked as required by the action but a default value must be there, so using `required` works
const PATH_INPUT = core.getInput('path', {required: true});
const FLAG_LIST_INPUT = core.getMultilineInput('flags', {required: true});
const FOLLOW_SYMLINK_INPUT = core.getBooleanInput('follow-symbolic-links', {required: true});

const groupDirectory = await core.group(
const trustedGroupDirectory = await core.group(
'Resolve group directory path',
async () => {
const res = path.resolve(PATH_INPUT, NAME_INPUT);
const res = trustedPathHelper.trust(path.join(PATH_INPUT, NAME_INPUT));
core.info('group directory=' + res);

return res;
}
);

const originalReportPaths = await core.group(
const trustedOriginalReportPaths = await core.group(
'Resolve reports',
async () => {
const result = [];
for await (const fp of globSDK.lookup(REPORTS_INPUT, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT})) {
const normalizedFp = pathSDK.relativeToGHWorkspace(fp);
for await (const fp of SDK.glob.lookup(REPORTS_INPUT, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT})) {
const normalizedFp = trustedPathHelper.toWorkspaceRelative(fp);
core.info('Found ' + normalizedFp);
result.push(normalizedFp);
}
return result;
}
);
core.debug('reports to copy=' + JSON.stringify(originalReportPaths));
core.debug('reports to copy=' + JSON.stringify(trustedOriginalReportPaths));

if (0 === originalReportPaths.length) {
if (0 === trustedOriginalReportPaths.length) {
core.setFailed('You must provide at least one report !');
}

const reportsMap = await core.group(
const trustedReportsMap = await core.group(
'Build reports map',
async () => {
let counter = 0;
return originalReportPaths.map(filepath => {
return trustedOriginalReportPaths.map(trustedSource => {
// Ensure report files uniqueness while keeping a bit of clarity regarding the mapping with original files !
const filename = path.basename(filepath) + '-report-' + (++counter);
const destination = pathSDK.relativeToGHWorkspace(groupDirectory, filename);
core.info(filepath + ' => ' + destination);
return {source: filepath, filename: filename, dest: destination};
const trustedFilename = path.basename(trustedSource) + '-report-' + (++counter); // Only trusted content !
const trustedDestination = path.join(trustedGroupDirectory, trustedFilename); // Only trusted content !

Check warning on line 55 in .github/actions/reports-group/create/index.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.github/actions/reports-group/create/index.js#L55

Detected possible user input going into a `path.join` or `path.resolve` function.
core.info(trustedSource + ' => ' + trustedDestination);

return {source: trustedSource, filename: trustedFilename, dest: trustedDestination};
});
}
);
core.debug('reports map=' + JSON.stringify(reportsMap));
core.debug('reports map=' + JSON.stringify(trustedReportsMap));

const metadata = await core.group(
const trustedMetadata = await core.group(
'Build group metadata',
async () => {
const res = {
name: NAME_INPUT,
format: FORMAT_INPUT,
reports: reportsMap.map(v => v.filename),
reports: trustedReportsMap.map(v => v.filename),
flags: FLAG_LIST_INPUT
};
core.info('Created');

return res;
}
);
core.debug('metadata=' + JSON.stringify(metadata));
core.debug('metadata=' + JSON.stringify(trustedMetadata));

await core.group('Create group directory', () => {
core.info('Create group directory at ' + groupDirectory);
core.info('Create group directory at ' + trustedGroupDirectory);

return io.mkdirP(groupDirectory)
return io.mkdirP(trustedGroupDirectory)
});

await core.group(
'Copy reports',
async () => reportsMap.map(async ({source, dest}) => {
core.info(source + ' => ' + dest);
async () => trustedReportsMap.map(async (trustedMap) => {
core.info(trustedMap.source + ' => ' + trustedMap.dest);

return io.cp(source, dest);
return io.cp(trustedMap.source, trustedMap.dest);
})
);

await core.group(
'Create metadata file',
async () => {
const filepath = path.join(groupDirectory, SDK_CONSTANTS.METADATA_FILENAME);
core.info('Create metadata file at ' + filepath + ' with: ' + JSON.stringify(metadata));
fs.writeFileSync(filepath, JSON.stringify(metadata));
const trustedFp = trustedPathHelper.trust(path.resolve(trustedGroupDirectory, SDK.METADATA_FILENAME));
core.info('Create metadata file at ' + trustedFp + ' with: ' + JSON.stringify(trustedMetadata));

fs.writeFileSync(trustedFp, JSON.stringify(trustedMetadata));

Check warning on line 101 in .github/actions/reports-group/create/index.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.github/actions/reports-group/create/index.js#L101

The application dynamically constructs file or path information.
});

const outputs = await core.group(
'Build action outputs',
async () => {
// Be sure to validate any path returned to the end-user !
const res = {};

core.info("Build 'path' output");
res.path = groupDirectory;
res.path = trustedPathHelper.trust(trustedGroupDirectory);
core.info("Build 'reports' output");
res.reports = metadata.reports.join('\n');
res.reports = trustedMetadata.reports.join('\n');
core.info("Build 'files' output");
res.files = originalReportPaths.join('\n');
res.files = trustedReportsMap.map(v => v.source).join('\n');

return res;
}
);
core.debug('outputs=' + JSON.stringify(outputs));
outputsSDK.bindActionOutputs(outputs);
SDK.outputs.bindFrom(outputs);
}

run();
4 changes: 2 additions & 2 deletions .github/actions/reports-group/find/dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/actions/reports-group/find/dist/index.js.map

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions .github/actions/reports-group/find/index.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
const core = require('@actions/core'); // @TODO move to 'imports from' when moved to TS !

const {find: findSdk, outputs: outputsSDK} = require('./node-sdk'); // @TODO move to 'imports from' when moved to TS !
const SDK = require('./node-sdk'); // @TODO move to 'imports from' when moved to TS !

async function run() {
const trustedPathConverter = SDK.path.trustedPathHelpers();
/** INPUTS **/
const PATH_INPUT = core.getInput('path', {required: true});
// Following inputs are not marked as required by the action but a default value must be there, so using `required` works
const FORMAT_INPUT = core.getInput('format', {required: true});
const GLUE_STRING_INPUT = core.getInput('glue-string', {required: true, trimWhitespace: false});
const FOLLOW_SYMLINK_INPUT = core.getBooleanInput('follow-symbolic-links', {required: true});

const groupDirPathList = await core.group(
const trustedGroupPaths = await core.group(
'Find groups',
async () => {
const groupDirPathList = await findSdk.groupPaths(PATH_INPUT, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT});
const res = (await SDK.find.trustedGroupPaths(PATH_INPUT, trustedPathConverter.toWorkspaceRelative, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT}));

groupDirPathList.forEach(p => core.info('Found a reports group directory at ' + p));
res.forEach(p => core.info('Found a reports group directory at ' + p));

return groupDirPathList;
return res;
}
);
core.debug('groupDirPathList=' + JSON.stringify(groupDirPathList));
if (0 === groupDirPathList.length) {
core.debug('Group paths=' + JSON.stringify(trustedGroupPaths));
if (0 === trustedGroupPaths.length) {
core.setFailed('Unable to retrieve any group. Something wrong most likely happened !');
}

Expand All @@ -31,13 +32,13 @@ async function run() {
const res = {};

core.info("Build 'list' output");
res.list = 'json' === FORMAT_INPUT ? JSON.stringify(groupDirPathList) : groupDirPathList.join(GLUE_STRING_INPUT)
res.list = 'json' === FORMAT_INPUT ? JSON.stringify(trustedGroupPaths) : trustedGroupPaths.join(GLUE_STRING_INPUT)

return res;
}
);
core.debug('outputs=' + JSON.stringify(outputs));
outputsSDK.bindActionOutputs(outputs);
SDK.outputs.bindFrom(outputs);
}

run();
4 changes: 2 additions & 2 deletions .github/actions/reports-group/load-metadata/dist/index.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

34 changes: 17 additions & 17 deletions .github/actions/reports-group/load-metadata/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,48 @@ const path = require('path'); // @TODO move to 'imports from' when moved to TS !

const core = require('@actions/core'); // @TODO move to 'imports from' when moved to TS !

const {find: findSDK, load: loadSDK, outputs: outputsSDK} = require('./node-sdk');
const SDK = require('./node-sdk'); // @TODO move to 'imports from' when moved to TS !

async function run() {
const trustedPathConverter = SDK.path.trustedPathHelpers();
/** INPUTS **/
const PATH_INPUT = core.getInput('path', {required: true});
// Following inputs are not marked as required by the action but a default value must be there, so using `required` works
const FORMAT_INPUT = core.getInput('format', {required: true});
const GLUE_STRING_INPUT = core.getInput('glue-string', {required: true, trimWhitespace: false});
const FOLLOW_SYMLINK_INPUT = core.getBooleanInput('follow-symbolic-links', {required: true});

const metadataList = await core.group(
const trustedMetadataList = await core.group(
'Build metadata list',
async () => {
const metadataPathList = await findSDK.metadataPaths(PATH_INPUT, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT});
if (0 === metadataPathList.length) {
const trustedMetadataPathList = await SDK.find.trustedGroupPaths(PATH_INPUT, trustedPathConverter.toWorkspaceRelative, {followSymbolicLinks: FOLLOW_SYMLINK_INPUT});
if (0 === trustedMetadataPathList.length) {
core.setFailed('Unable to retrieve any group. Something wrong most likely happened !');
}

return Promise.all(
metadataPathList.map(async (fp) => {
core.info('Load '+ fp);
return trustedMetadataPathList.map(async (trustedGroupPath) => {
core.info('Load '+ trustedGroupPath);

return loadSDK.metadataFile(fp);
})
);
return trustedPathConverter.trustedMetadataUnder(trustedGroupPath);
});
}
);
core.debug('metadataList=' + JSON.stringify(metadataList));
core.debug('Group paths=' + JSON.stringify(trustedMetadataList));

const outputs = await core.group(
'Build action outputs',
async () => {
const res = {};

// @TODO move back to dedicated properties (merge array/object properties one by one in case of multi result with json output)
core.info("Build 'metadata' output");
if ('json' === FORMAT_INPUT) {
// Detect if provided `paths` was a group directory
const isSingleMetadata = metadataList.length === 1 && path.resolve(metadataList[0].path) === path.resolve(PATH_INPUT);
res.metadata = isSingleMetadata ? metadataList.shift() : metadataList;
// Detect if provided `paths` was an actual group directory
const isSingleMetadata = trustedMetadataList.length === 1 && path.resolve(trustedMetadataList[0].path) === path.resolve(PATH_INPUT);
res.metadata = isSingleMetadata ? trustedMetadataList.shift() : trustedMetadataList;
} else {
const formatScalar = (key) => [...(new Set(metadataList.map(m => m[key]))).values()].join(GLUE_STRING_INPUT);
const formatList = (key) => [...(new Set(metadataList.map(m => m[key]).flat())).values()].join(GLUE_STRING_INPUT);
const formatScalar = (key) => [...(new Set(trustedMetadataList.map(m => m[key]))).values()].join(GLUE_STRING_INPUT);
const formatList = (key) => [...(new Set(trustedMetadataList.map(m => m[key]).flat())).values()].join(GLUE_STRING_INPUT);

res.metadata = {
name: formatScalar('name'),
Expand All @@ -59,7 +59,7 @@ async function run() {
}
);
core.debug('outputs=' + JSON.stringify(outputs));
outputsSDK.bindActionOutputs(outputs);
SDK.outputs.bindFrom(outputs);
}

run();
8 changes: 4 additions & 4 deletions .github/actions/reports-group/node-sdk/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
export * as CONSTANTS from './src/constants';
export * as glob from './src/glob-helper'
export * as path from './src/path-helper'
export * as glob from './src/glob'
export * as find from './src/find'
export * as load from './src/load'
export * as outputs from './src/outputs'
export * as path from './src/path'

export * from './src/constants';

// @TODO try yarn workspace again (keep node-sdk as is at first !)
// @TODO If yarn workspace works, remove node-sdk symlink and replace with a requirement with `workspace:^`
Expand Down
30 changes: 21 additions & 9 deletions .github/actions/reports-group/node-sdk/src/find.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,37 @@ const path = require('path'); // @TODO move to 'imports from' when moved to TS !
const core = require('@actions/core'); // @TODO move to 'imports from' when moved to TS !

const {METADATA_FILENAME} = require('./constants');
const globHelper = require('./glob-helper');
const pathHelper = require('./path-helper');
const glob = require('./glob');

export async function groupPaths(globPattern, options = undefined) {
const absWorkspace = path.resolve('.');
/**
@param {string} globPattern
@param {function(untrusted: string): string} toTrustedPath A function ensuring path is valid before returning it
@param {import('@actions/glob').GlobOptions|undefined} globOptions
@returns {Promise<string[]>} Trusted groups directory path list
*/
export async function trustedGroupPaths(globPattern, toTrustedPath, globOptions = undefined) {
const list = [];
for (const fp of await metadataPaths(globPattern, options)) {
list.push(pathHelper.relativeTo(absWorkspace, path.dirname(fp)));
for (const fp of await trustedMetadataPaths(globPattern, toTrustedPath, globOptions)) {
list.push(path.dirname(fp));
}

return list;
}

export async function metadataPaths(globPattern, options = undefined) {
const finalPattern = globPattern.split('\n').map(item => path.join(item.trim(), '**', METADATA_FILENAME)).join('\n');
/**
* @param {string} globPattern
* @param {function(untrusted: string): string} toTrustedPath A function ensuring path is valid before returning it
* @param {import('@actions/glob').GlobOptions|undefined} globOptions
*
* @returns {Promise<string[]>} Trusted metadata path list
*/
export async function trustedMetadataPaths(globPattern, toTrustedPath, globOptions = undefined) {
const finalPattern = globPattern.split('\n').map(item => toTrustedPath(path.join(item.trim(), '**', METADATA_FILENAME))).join('\n');

Check warning on line 32 in .github/actions/reports-group/node-sdk/src/find.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

.github/actions/reports-group/node-sdk/src/find.js#L32

Detected possible user input going into a `path.join` or `path.resolve` function.
core.debug('Find metadata paths with ' + globPattern);

const list = [];
for await (const fp of globHelper.lookup(finalPattern, options)) {
for await (const fp of glob.lookup(finalPattern, globOptions)) {
list.push(fp);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
const glob = require('@actions/glob'); // @TODO move to 'imports from' when moved to TS !

/**
* /!\ Returns *untrusted* paths as the pattern is not validated /!\
*
* @param {string} pattern
* @param {import('@actions/glob').GlobOptions|undefined} options
*
* @returns {AsyncGenerator<string, void>}
*/
export async function* lookup(pattern, options = undefined) {
const finalOptions = {
followSymbolicLinks: options?.followSymbolicLinks ?? true,
Expand Down
Loading

0 comments on commit 17a13b1

Please sign in to comment.