Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for flags & options protection #908

Merged
merged 35 commits into from
Jun 11, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
27fe51d
API design for flag/option "escaping"
ericcornelissen May 30, 2023
3576fb4
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen May 30, 2023
7d6aa1d
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 4, 2023
624f322
Initial flag protection implementation
ericcornelissen Jun 4, 2023
08ba83e
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 4, 2023
95ecdd8
Support integration tests for flag protection on Windows
ericcornelissen Jun 4, 2023
e519a55
Corrections
ericcornelissen Jun 4, 2023
9ba174e
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 5, 2023
ca8c7fc
Move flag protection execution into main.js
ericcornelissen Jun 5, 2023
7f087e5
Expand `flagFixtures`
ericcornelissen Jun 5, 2023
92328e6
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 5, 2023
aa0a8b6
Revert unintended changes
ericcornelissen Jun 5, 2023
6ba3f0c
Fix flag protection issue with escaped chars before flag chars
ericcornelissen Jun 5, 2023
b07aed3
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 6, 2023
5039bc4
Change `getQuoteFunction` to return an escape-quote pair
ericcornelissen Jun 7, 2023
291bb02
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 7, 2023
86e27e1
Unit test `stripFlagPrefix` implementations
ericcornelissen Jun 7, 2023
c316177
Cover edge case w.r.t. flag protection occurring on Windows
ericcornelissen Jun 7, 2023
8433c61
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 7, 2023
875127e
Revert c316177615cbca1adbb65c5f1377d82282154f56
ericcornelissen Jun 8, 2023
693f1d1
Switch to Shell-specific `stripFlagPrefix` functions
ericcornelissen Jun 8, 2023
f530ec7
Documentation correction
ericcornelissen Jun 8, 2023
9eebac4
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 8, 2023
a5a9d63
Adjust tests to shell-specific `stripFlagPrefix`
ericcornelissen Jun 9, 2023
d08fb3b
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 9, 2023
8c74b82
Rename "stripFlagPrefix" in general
ericcornelissen Jun 9, 2023
0fdf79e
Improve internal documentation
ericcornelissen Jun 9, 2023
65778f3
Reduce overall diff and align unit tests
ericcornelissen Jun 9, 2023
5b3038a
Update testing around flag protection
ericcornelissen Jun 9, 2023
748cae9
Corrections
ericcornelissen Jun 9, 2023
bbd965b
Correct unit test titles
ericcornelissen Jun 11, 2023
b149e1d
Update the changelog
ericcornelissen Jun 11, 2023
10b98b4
Update tips documentation
ericcornelissen Jun 11, 2023
7459fa5
Merge branch 'main' into 452-escape-flags-and-options
ericcornelissen Jun 11, 2023
144a025
Document `flagProtection`, correct `interpolation` docs
ericcornelissen Jun 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
22 changes: 22 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ type ShellOption = boolean | string | undefined;
* Options for {@link escape} and {@link escapeAll}.
*/
interface EscapeOptions {
/**
* Whether or not to protect against flag and option (such as `--verbose`)
* injection
*
* @default false
* @since 1.7.0
*/
readonly flagProtection?: boolean;

/**
* Is interpolation enabled.
*
Expand All @@ -35,6 +44,15 @@ interface EscapeOptions {
* Options for {@link quote} and {@link quoteAll}.
*/
interface QuoteOptions {
/**
* Whether or not to protect against flag and option (such as `--verbose`)
* injection.
*
* @default false
* @since 1.7.0
*/
readonly flagProtection?: boolean;

/**
* The shell to escape for.
*
Expand All @@ -58,6 +76,7 @@ interface QuoteOptions {
* );
* @param {string} arg The argument to escape.
* @param {object} [options] The escape options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean} [options.interpolation=false] Is interpolation enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string} The escaped argument.
Expand All @@ -82,6 +101,7 @@ export function escape(arg: string, options?: EscapeOptions): string;
* );
* @param {string[]} args The arguments to escape.
* @param {object} [options] The escape options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean} [options.interpolation=false] Is interpolation enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string[]} The escaped arguments.
Expand Down Expand Up @@ -115,6 +135,7 @@ export function escapeAll(args: string[], options?: EscapeOptions): string[];
* );
* @param {string} arg The argument to quote and escape.
* @param {object} [options] The escape and quote options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string} The quoted and escaped argument.
* @throws {TypeError} The argument is not stringable.
Expand All @@ -140,6 +161,7 @@ export function quote(arg: string, options?: QuoteOptions): string;
* );
* @param {string[]} args The arguments to quote and escape.
* @param {object} [options] The escape and quote options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string[]} The quoted and escaped arguments.
* @throws {TypeError} One of the arguments is not stringable.
Expand Down
4 changes: 4 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function getPlatformHelpers() {
* );
* @param {string} arg The argument to escape.
* @param {object} [options] The escape options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean} [options.interpolation=false] Is interpolation enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string} The escaped argument.
Expand Down Expand Up @@ -70,6 +71,7 @@ export function escape(arg, options = {}) {
* );
* @param {string[]} args The arguments to escape.
* @param {object} [options] The escape options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean} [options.interpolation=false] Is interpolation enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string[]} The escaped arguments.
Expand Down Expand Up @@ -106,6 +108,7 @@ export function escapeAll(args, options = {}) {
* );
* @param {string} arg The argument to quote and escape.
* @param {object} [options] The escape and quote options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string} The quoted and escaped argument.
* @throws {TypeError} The argument is not stringable.
Expand Down Expand Up @@ -134,6 +137,7 @@ export function quote(arg, options = {}) {
* );
* @param {string[]} args The arguments to quote and escape.
* @param {object} [options] The escape and quote options.
* @param {boolean} [options.flagProtection=false] Is flag protection enabled.
* @param {boolean | string} [options.shell] The shell to escape for.
* @returns {string[]} The quoted and escaped arguments.
* @throws {TypeError} One of the arguments is not stringable.
Expand Down
71 changes: 56 additions & 15 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ function checkedToString(value) {
*
* @param {object} args The arguments for this function.
* @param {object} args.options The options for escaping.
* @param {boolean | string} [args.options.shell] The shell to escape for.
* @param {boolean} [args.options.flagProtection] Is flag protection enabled.
* @param {boolean} [args.options.interpolation] Is interpolation enabled.
* @param {boolean | string} [args.options.shell] The shell to escape for.
* @param {object} args.process The `process` values.
* @param {object} args.process.env The environment variables.
* @param {object} deps The dependencies for this function.
Expand All @@ -47,51 +48,79 @@ function checkedToString(value) {
* @returns {object} The parsed arguments.
*/
function parseOptions(
{ options: { interpolation, shell }, process: { env } },
{ options: { flagProtection, interpolation, shell }, process: { env } },
{ getDefaultShell, getShellName }
) {
flagProtection = flagProtection ? true : false;
interpolation = interpolation ? true : false;
shell = isString(shell) ? shell : getDefaultShell({ env });

const shellName = getShellName({ shell }, { resolveExecutable });
return { interpolation, shellName };
return { flagProtection, interpolation, shellName };
}

/**
* Escapes an argument for the given shell.
*
* @param {object} args The arguments for this function.
* @param {string} args.arg The argument to escape.
* @param {boolean} args.flagProtection Is flag protection enabled.
* @param {boolean} args.interpolation Is interpolation enabled.
* @param {string} args.shellName The name of the shell to escape `arg` for.
* @param {object} deps The dependencies for this function.
* @param {Function} deps.getEscapeFunction Get the escape function for a shell.
* @param {Function} deps.stripFlagPrefix A function to strip flag prefixes.
* @returns {string} The escaped argument.
* @throws {TypeError} The argument to escape is not stringable.
*/
function escape({ arg, interpolation, shellName }, { getEscapeFunction }) {
function escape(
{ arg, flagProtection, interpolation, shellName },
{ getEscapeFunction, stripFlagPrefix }
) {
const argAsString = checkedToString(arg);
const escape = getEscapeFunction(shellName, { interpolation });
const escapedArg = escape(argAsString);
return escapedArg;
if (flagProtection) {
return stripFlagPrefix(escapedArg);
} else {
return escapedArg;
}
}

/**
* Quotes and escape an argument for the given shell.
*
* @param {object} args The arguments for this function.
* @param {string} args.arg The argument to escape.
* @param {boolean} args.flagProtection Is flag protection enabled.
* @param {string} args.shellName The name of the shell to escape `arg` for.
* @param {object} deps The dependencies for this function.
* @param {Function} deps.getQuoteFunction Get the quote function for a shell.
* @param {Function} deps.stripFlagPrefix A function to strip flag prefixes.
* @returns {string} The quoted and escaped argument.
* @throws {TypeError} The argument to escape is not stringable.
*/
function quote({ arg, shellName }, { getQuoteFunction }) {
function quote(
{ arg, flagProtection, shellName },
{ getQuoteFunction, stripFlagPrefix }
) {
const argAsString = checkedToString(arg);
const quote = getQuoteFunction(shellName);
const escapedAndQuotedArg = quote(argAsString);
return escapedAndQuotedArg;
if (flagProtection) {
return (
escapedAndQuotedArg.substring(0, 1) +
stripFlagPrefix(
escapedAndQuotedArg.substring(1, escapedAndQuotedArg.length - 1)
) +
escapedAndQuotedArg.substring(
escapedAndQuotedArg.length - 1,
escapedAndQuotedArg.length
)
);
ericcornelissen marked this conversation as resolved.
Show resolved Hide resolved
} else {
return escapedAndQuotedArg;
}
}

/**
Expand All @@ -100,6 +129,7 @@ function quote({ arg, shellName }, { getQuoteFunction }) {
* @param {object} args The arguments for this function.
* @param {string} args.arg The argument to escape.
* @param {object} args.options The options for escaping `arg`.
* @param {boolean} [args.options.flagProtection] Is flag protection enabled.
* @param {boolean} [args.options.interpolation] Is interpolation enabled.
* @param {boolean | string} [args.options.shell] The shell to escape `arg` for.
* @param {object} args.process The `process` values.
Expand All @@ -108,23 +138,25 @@ function quote({ arg, shellName }, { getQuoteFunction }) {
* @param {Function} deps.getDefaultShell Function to get the default shell.
* @param {Function} deps.getEscapeFunction Get an escape function for a shell.
* @param {Function} deps.getShellName Function to get the name of a shell.
* @param {Function} deps.stripFlagPrefix A function to strip flag prefixes.
* @returns {string} The escaped argument.
*/
export function escapeShellArg(
{ arg, options: { interpolation, shell }, process: { env } },
{ getDefaultShell, getEscapeFunction, getShellName }
{ arg, options: { flagProtection, interpolation, shell }, process: { env } },
{ getDefaultShell, getEscapeFunction, getShellName, stripFlagPrefix }
) {
const options = parseOptions(
{ options: { interpolation, shell }, process: { env } },
{ options: { flagProtection, interpolation, shell }, process: { env } },
{ getDefaultShell, getShellName }
);
return escape(
{
arg,
flagProtection: options.flagProtection,
interpolation: options.interpolation,
shellName: options.shellName,
},
{ getEscapeFunction }
{ getEscapeFunction, stripFlagPrefix }
);
}

Expand All @@ -134,22 +166,31 @@ export function escapeShellArg(
* @param {object} args The arguments for this function.
* @param {string} args.arg The argument to escape.
* @param {object} args.options The options for escaping `arg`.
* @param {boolean} [args.options.flagProtection] Is flag protection enabled.
* @param {boolean | string} [args.options.shell] The shell to escape `arg` for.
* @param {object} args.process The `process` values.
* @param {object} args.process.env The environment variables.
* @param {object} deps The dependencies for this function.
* @param {Function} deps.getDefaultShell Function to get the default shell.
* @param {Function} deps.getQuoteFunction Get a quote function for a shell.
* @param {Function} deps.getShellName Function to get the name of a shell.
* @param {Function} deps.stripFlagPrefix A function to strip flag prefixes.
* @returns {string} The quoted and escaped argument.
*/
export function quoteShellArg(
{ arg, options: { shell }, process: { env } },
{ getDefaultShell, getQuoteFunction, getShellName }
{ arg, options: { flagProtection, shell }, process: { env } },
{ getDefaultShell, getQuoteFunction, getShellName, stripFlagPrefix }
) {
const options = parseOptions(
{ options: { shell }, process: { env } },
{ options: { flagProtection, shell }, process: { env } },
{ getDefaultShell, getShellName }
);
return quote({ arg, shellName: options.shellName }, { getQuoteFunction });
return quote(
{
arg,
flagProtection: options.flagProtection,
shellName: options.shellName,
},
{ getQuoteFunction, stripFlagPrefix }
);
}
11 changes: 11 additions & 0 deletions src/unix/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,14 @@ export function getShellName({ shell }, { resolveExecutable }) {

return shellName;
}

/**
* Remove any prefix from the provided argument that might be interpreted as a
* flag on Unix systems.
*
* @param {string} arg The argument to update.
* @returns {string} The updated argument.
*/
export function stripFlagPrefix(arg) {
return arg.replace(/^-+/gu, "");
}
11 changes: 11 additions & 0 deletions src/win/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,14 @@ export function getShellName({ shell }, { resolveExecutable }) {

return shellName;
}

/**
* Remove any prefix from the provided argument that might be interpreted as a
* flag on Windows systems.
*
* @param {string} arg The argument to update.
* @returns {string} The updated argument.
*/
export function stripFlagPrefix(arg) {
return arg.replace(/^(?:-+|\/+)/gu, "");
}
8 changes: 7 additions & 1 deletion test/_arbitraries.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,13 @@ export const shescapeOptions = () =>
fc.object({
key: fc.oneof(
fc.string(),
fc.constantFrom("interpolation", "quoted", "shell", "shellName")
fc.constantFrom(
"flagProtection",
"interpolation",
"quoted",
"shell",
"shellName"
)
),
values: [
fc.boolean(),
Expand Down