-
Notifications
You must be signed in to change notification settings - Fork 922
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
fix: allow process.env
in ignores
#4282
fix: allow process.env
in ignores
#4282
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. |
Sure @escapedcat let me add my review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey @SpenserJ I totally get the point of this PR, the specific case you mentioned should be allowed. However, I'm a bit concerned about the security implications of leaving the process.env
object accessible, because it could contain sensitive information or be used as part of a larger attack chain (think of process.env.API_KEY
)
So while the current use case for process.env.CI
seems legitimate, the implementation allows access to all environment variables, which is much broader than necessary.
Could we think of a better approach? Like having a whitelist of allowed env variables?
Something like:
const ALLOWED_ENV_VARS = ['CI', 'NODE_ENV'];
And then we could check if the fnString
includes process
then we only allow accessing to those vars, otherwise we throw. Then we do the current regExp check.
What do you think?
Hey @edodusi, thank you for the feedback. I'd be happy to make that change, and am wondering if you'd prefer it to be hardcoded or configurable, and if its configurable what config name would you like it to use? |
@SpenserJ I think if we leave it configurable we can open the same security hole, so I would probably go with hardcode (also the regexp itself is an hardcoded blacklist). At the moment I can only think of scenarios that involve |
@edodusi I'm not sure I fully understand the security hole and would love it if you could teach me more about the potential risks. I assume most security-conscious repos are already restricting automatic CI runs to trusted contributors or reviewed PRs (like commitlint does), and an upstream supply chain attack would either be able to access it directly or shouldn't be able to change the configuration for commitlint. My team isn't currently using any other env variables for commitlint, but I'm not sure thats a good reason to hardcode it since the original PR didn't account for my needs and we may not be accounting for someone else's legitimate needs by hardcoding either. That said, I can't think of a case where you'd only want to ignore a commit message outside of CI (I can't even think of a scenario where you'd only ignore in a certain |
@SpenserJ so my concern (and the reason for my PR #4258) is that users can include commitlint rules provided by third parties, say for example This is to summarize, and to answer your question: if we let the author of the You may say that the end user could simply look at the code they are including in I agree with you that here we are deciding that certain use cases are valid and others are not, but honestly I cannot come up with a better idea ATM |
@edodusi Thank you, that makes sense 👍 While working on this, I did notice a few issues with the overall implementation:
Doing a simple string check will catch a basic attack, but its very easy to get around and you can confirm this by extending a config containing: // Import at the top level isn't caught by checking the `ignores` function
// Using `as` lets us alias the import to further hide from checks
import { execSync as bypass } from 'child_process';
export default {
ignores: [
// Able to call child_process.execSync
commit => { console.log(bypass('ls', { encoding: 'utf8' })); return true; },
],
}; Next to using an AST to scan the whole config, I can't think of a good way of preventing bypasses like this. The checks are still useful for blocking naive exploitation and supply chain attacks, but they're insufficient for preventing a more experienced attacker. Even if we ignore spawning processes, it would be very easy to bypass most string-based environment guards checking for export default {
ignores: [
commit => {
// Destructuring avoids any checks for `process.`
const { env: { API_KEY: exploited } } = process;
console.log(exploited);
return true;
},
],
}; Although I agree with trying to prevent a supply chain attack like this, I don't think this approach is sufficient and I'm not even sure that a more advanced approach like using an AST to track variable assignments and destructuring would prevent most bypasses without being extremely complicated and expensive to maintain |
@SpenserJ totally agree here: this level of protection is very basic, however at least it's something? My concern is that since this is a library that validates commits it could be perceived as "innocent" by end users, and because of that the level of attention is lower. But this is a script that people executes on their local env and in CI, so to me it's maximum alert 😄 Do you have any other idea on how to improve the security level here? Could the cc @escapedcat |
Nor really my expertise to be honest. I'd rather not make something opt-in. Maybe if people know what they are doing they can disable it? |
@escapedcat and what about, instead of a function, letting users provide a regexp? If the regexp matches then the commit is ignored, which I think is probably the most common use case. You could also provide other options like ignore if a certain env variable equals a value, both provided by the user. |
@edodusi @escapedcat I see it from the opposite side, where very basic protection that is easy to bypass is worse than no protection at all, since its a false sense of security. No matter how innocent the dependency, end users should be responsible for knowing what they're running and taking adequate steps to protect their repos and secrets. I don't disagree that most users won't look close enough (or at all) to understand the risks, but I don't think its reasonable or possible to prevent every possible supply chain attack, and an easily bypassed check is more dangerous than "Run untrusted code at your own risk" The only ways I can think of implementing this check more effectively would be either AST scanning the entire package that is being extended (technically and computationally expensive, and better left to SAST and vulnerability scanning tools) or limiting configs/presets/plugins/etc to a syntax like JSON that can't execute code (which greatly limits users and preset maintainers from doing legitimate things). Letting users provide a regexp instead of a function seems like a nice improvement for usability since I agree that its the most common use-case, but that doesn't prevent the supply chain concern you mentioned. My suggestion would be to remove the easily bypassed check since it prevents legitimate uses and fails to prevent the intended risks, and potentially remind users that running untrusted code is dangerous (which everyone should already understand). For now I'll be working around it in our repos by destructuring |
@SpenserJ by providing only a regexp the most dangerous supply chain risk will be prevented (executing arbitrary code), I'm thinking about removing Anyway, I get your point, I don't know if no security is better than low security, I will leave this to another reviewer but thank you for your very clear explanation |
@edodusi That doesn't prevent the most dangerous supply chain risk if presets are still able to run JS outside of the ignores function though import { exec } from 'child_process';
exec('ls'); // Supply chain attack in the preset
export default {
ignores: [
/^wip: /, // Only allow regexp in ignores
],
}; |
@SpenserJ oh yes, yes you are right. So at this point do you think it's better to just revert my PR and leave it to the end user completely? |
@edodusi As much as I hate to see contributions reversed, I would recommend reverting it unless someone can find a more complete solution. It could be helpful to check through other libraries that use config presets like ESLint/Babel/etc and see what they do. I'm just a random contributor that ran into a limitation with this approach though, so that is a better question for the maintainers (cc @escapedcat). Regardless of the outcome, kudos to you for trying to improve the security 👏 I wish users could be trusted to do their own research, and its refreshing to see someone making security conscious improvements to protect users! |
Thank you @SpenserJ. I'm leaving the decision to @escapedcat |
@escapedcat Any thoughts on this? Tl;dr is that the added security around execution in |
Ugh, I kinda tried to sit this out, sorry. |
@escapedcat I'm of the opinion that #4258 should be reverted, since it gives a false sense of security and prevents legitimate use without actually preventing the intended security risks. This PR relaxes the check slightly, but its still extremely easy to work around the check as demonstrated in #4282 (comment), so I'm not sure those checks are worth the complexity |
Alright, thanks for your feedback! Appreciate both of your comments. |
Thanks for the update. In that case I'll close this PR, but I'll leave #4281 open in case anyone runs into it until the new version is released |
Description
This adjusts the
dangerousPattern
regexp to allowprocess.env
while preventing otherprocess
referencesMotivation and Context
This fixes #4281
Usage examples
How Has This Been Tested?
An additional test case has been added, and it has been tested in our projects
Types of changes
Checklist: