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

JENKINS-46670: Escape quotes in environment variables #218

Merged
merged 4 commits into from
Oct 16, 2017

Conversation

MattLud
Copy link
Contributor

@MattLud MattLud commented Sep 6, 2017

Simple escape for quotes on environment variables to avoid dangling multi-line variables during 'export' part of container.

@marvinthepa
Copy link
Contributor

Does this break with already escaped quotes?

See https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Escaping_JavaScript_escapes.

Would be nice if you could add a test for that.

IMHO it is always better to use a battle tested library for stuff like that, instead of implementing it with string replace..

Copy link
Contributor

@marvinthepa marvinthepa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use a method of escaping that takes into account already escaped values..

@@ -212,7 +212,7 @@ public void onClose(int i, String s) {
environmentExpander.expand(envVars);
for (Map.Entry<String, String> entry : envVars.entrySet()) {
watch.getInput().write(
String.format("export %s=\"%s\"%s", entry.getKey(), entry.getValue(), NEWLINE).getBytes(StandardCharsets.UTF_8));
String.format("export %s=\"%s\"%s", entry.getKey(), entry.getValue().replace("\"","\\\""), NEWLINE).getBytes(StandardCharsets.UTF_8));
Copy link
Contributor

@marvinthepa marvinthepa Sep 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked, this is vulnerable to double escaping:

podTemplate(name: "horst", label: "label", containers: [
                        containerTemplate(name: 'ubuntu',
                                image: 'ubuntu',
                                ttyEnabled: true,
                                command: 'cat',
                        )
]) {
    withEnv(['C="C', 'D=D"', 'E=\\"E', 'F=F\\"']) {
        node("label") {
            container('ubuntu') {
                sh '''echo "C='$C'" '''
                sh '''echo "D='$D'" '''
                sh '''echo "E='$E'" '''
                sh '''echo "F='$F'" '''
            }
        }
    }
}

leads to

[full] Running shell script
+ echo C='"C'
C='"C'
[Pipeline] sh
[full] Running shell script
+ echo D='D"'
D='D"'
[Pipeline] sh
[full] Running shell script
+ echo E='\E
export F=F\'
E='\E
export F=F\'
[Pipeline] sh
[full] Running shell script
+ echo F=''
F=''

@marvinthepa
Copy link
Contributor

marvinthepa commented Sep 8, 2017

Note: corrected the example, '\\' is necessary to trigger this.

While not necessarily a vulnerability, users might trigger unexpected errors when using escaped quotes.

@MattLud
Copy link
Contributor Author

MattLud commented Sep 8, 2017

Agreed - switched to using StringEscapeUtils and added test cases.

}

if (environmentExpander != null) {
EnvVars envVars = new EnvVars();
environmentExpander.expand(envVars);
for (Map.Entry<String, String> entry : envVars.entrySet()) {
watch.getInput().write(
String.format("export %s=\"%s\"%s", entry.getKey(), entry.getValue(), NEWLINE).getBytes(StandardCharsets.UTF_8));
String.format("export %s=\"%s\"%s", entry.getKey(), StringEscapeUtils.escapeJavaScript(entry.getValue()), NEWLINE).getBytes(StandardCharsets.UTF_8));
Copy link
Contributor

@marvinthepa marvinthepa Sep 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This escapes single quotes as well, which

a) does not even work in bash
b) is wrong as we are already in double quotes (%s=\"%s\"%s")

see e.g. a password that contains a single quote (stupid idea, but happens), where it will be encoded as \' (literally).

example pipeline:

podTemplate(name: "horst", label: "label", containers: [
                        containerTemplate(name: 'ubuntu',
                                image: 'ubuntu',
                                ttyEnabled: true,
                                command: 'cat',
                        )
]) {
    withEnv(["C='C'", 'D=\'', 'E=	', 'F=\n']) {
        node("label") {
            container('ubuntu') {
                sh '''#!/bin/bash -x
ENC='Jw=='; [[ $D = $(base64 -d <<<$ENC) ]]
'''
            }
        }
    }
}

Note: 'Jw==' is a single quote base64 encoded.

The single quote part can be fixed by using escapeJava instead of escapeJavaScript, but I don't know if that will have more issues. It also escapes "quotes and control-chars (tab, backslash, cr, ff, etc.)", see Javadoc. It also escapes unicode characters, see source code :-).

Escaping is hard, especially for bash. Maybe it makes sense to add more tests, especially regarding control chars and unicode.

Copy link
Contributor Author

@MattLud MattLud Sep 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Back now and able to look at this - I had originally thought of the issue of blasting these out to build console log and thought to use the javascript to escape - how wrong that was!

re: xss - I check and the console log viewer escapes already

export HACK="<script>alert('XSS);</script>"

becomes

export HACK="&lt;script&gt;alert('XSS');&lt;\/script&gt;" 

so I think we should be good there unless I'm not seeing it right.

To sum up: We want to only escape double quotes if they exist in the environment variable, not anything else with it - otherwise, we're guessing at what the user REALLY meant to escape. If the user provided an escaped quote such as

  environment {
   //note that pipeline requires that extra \
    quote = 'Testing-\\"-qoute' 
    }

it comes to bash as
quote = 'Testing-"-qoute'
and we should provide the same functionality as the other executors provide by putting executing export quote = "Testing-\"-qoute"
and seeing
quote = 'Testing-"-qoute'

So I'm not sure where to go from here other than making naive assumption to ONLY escape a double quote in the provided variable then let bash double quotes handle the rest.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

XSS is not an issue here, I never wanted to hint at that. OWASP is just a nice resource for escaping issues of any kind 😃 .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm not sure where to go from here

I would suggest to use escapeJava instead of escapeJavascript, and add a few test cases to make sure that whitespace characters (tab, backslash, cr, ff, etc.) are encoded correctly. See https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/StringEscapeUtils.html#escapeJava(java.lang.String) for reference.

@scoheb
Copy link
Contributor

scoheb commented Oct 11, 2017

@MattLud
Copy link
Contributor Author

MattLud commented Oct 11, 2017

This is updated now - I'll have the candidate fix for "global properties" variables behind it since they'll use that common escape.

@carlossg
Copy link
Contributor

@marvinthepa looks like your feedback has been addressed, can we merge? I'd like to cut a new release

@marvinthepa
Copy link
Contributor

@MattLud looks better.

Did you check what happens with whitespace/control characters?

See https://commons.apache.org/proper/commons-lang/javadocs/api-3.1/org/apache/commons/lang3/StringEscapeUtils.html#escapeJava(java.lang.String):

Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.)

So a tab becomes the characters '\' and 't'.

@marvinthepa
Copy link
Contributor

Looks like this is one cause for "script returned exit code -1", see https://issues.jenkins-ci.org/browse/JENKINS-46651

@marvinthepa
Copy link
Contributor

marvinthepa commented Oct 12, 2017

@carlossg I would still like to see a test that verifies the correct handling of how tabs, newlines and such.

I am all for a new release, though, I'll check manually. Could we also have #232 in the release?

@marvinthepa
Copy link
Contributor

@carlossg @MattLud whitespace is not correctly handled:

podTemplate(name: "horst", label: "label", containers: [
                        containerTemplate(name: 'ubuntu',
                                image: 'ubuntu',
                                ttyEnabled: true,
                                command: 'cat',
                        )
]) {
    withEnv(["NL=before newline\nafter newline", 'TAB=before tab	after tab', '''F=before newline
after newline''']) {
        node("label") {
            container('ubuntu') {
                sh '''#!/bin/bash -x
echo NL $NL
echo TAB $TAB
echo F $F
'''
            }
        }
    }
}

leads to

[full] Running shell script
+ echo NL before 'newline\nafter' newline
NL before newline\nafter newline
+ echo TAB before 'tab\tafter' tab
TAB before tab\tafter tab
+ echo F before 'newline\nafter' newline
F before newline\nafter newline

I.e., a literal newline becomes the characters '' and 'n', while it should be a literal newline.

@marvinthepa
Copy link
Contributor

@MattLud

Maybe escapeXsi is more useful here?

@carlossg would it be okay to add an apache commons-text dependency?

@MattLud
Copy link
Contributor Author

MattLud commented Oct 12, 2017

EscapeXSI was no better -

F='before\ newlineafter\ newline'
NL='before\ newlineafter\ newline'
TAB='before\ tab\	after\ tab'

@marvinthepa
Copy link
Contributor

That is annoying. If only kubernetes would provide an API to set environment variables directly, we wouldn't have this problem..

@marvinthepa
Copy link
Contributor

marvinthepa commented Oct 13, 2017

Although this goes against all I said earlier, this seems to work to only escape double quotes, and prevent double escaping:

/* entry.getValue().replaceAll("\\\\([\\s\\S])|(\")", "\\\\$1$2") */ // this is wrong

See https://gist.github.com/getify/3667624 for reference.

But I guess this needs extensive tests.

@MattLud do you have the time to try that out and/or write tests for this?

Edited: this was wrong, sorry about that.

@scoheb
Copy link
Contributor

scoheb commented Oct 13, 2017

I noticed there is also another problem...it seems when you throw global libraries into the mix, it creates env vars for each one that gets loaded...

I see this in my logs:

export: `library.ci-pipeline.version=debug-int-tests': not a valid identifier

'ci-pipeline' is my library...and the version is my branch which is 'debug-int-tests'

I patched my env with something simple like:

key.replaceAll("\\.", "_").replaceAll("-", "_")

make sense?

@marvinthepa
Copy link
Contributor

After a discussion with a security-aware colleague of mine, we came up with a different approach.

I will push it for you to review.

It involves a self-implemented escaper, but it is a very simple one.

@marvinthepa
Copy link
Contributor

marvinthepa commented Oct 13, 2017

@scoheb:

when you throw global libraries into the mix

Could you provide a code sample, so I can reproduce it? Thanks.

I tried something like

@Library('horst.kevin') _

Before the whole pipeline, but it didn't trigger any error for me.

That way, we only need to escape single quotes, nothing else.

Single quotes cannot be escaped using baslash, as that is treated
literally inside single quotes.
I.e. to get a literal single quotes, we have to break out of the
single quotes and escape it outside. "Simple".

So "it's cool" becomes 'it'\''s cool,i',

Treated by the shell as the concatenation of "it", "'", and "s cool".

See e.g. https://stackoverflow.com/a/1315213.
@marvinthepa
Copy link
Contributor

@MattLud @carlossg please see 3b2132b.

@scoheb
Copy link
Contributor

scoheb commented Oct 13, 2017

@marvinthepa

Trying to put together something for the env vars from libraries...nothing yet...

but, take a look at this: https://github.com/jenkinsci/workflow-cps-global-lib-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/workflow/libs/LibraryConfiguration/help-name.html

@marvinthepa
Copy link
Contributor

@scoheb: I was able to reproduce your problem (just using withEnv(["a.b=c"])), but just together with #232, so I suggest solving that problem there (or in a new PR that @MattLud might open for that).

@carlossg do you have time to review my change and merge to master, so that we can merge it into #232?

@carlossg
Copy link
Contributor

@marvinthepa so this is ready to merge now?

@carlossg
Copy link
Contributor

there's a findbugs error

[INFO] Format string should use %n rather than \n in org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator$1.setupEnvironmentVariable(EnvVars, ExecWatch) [org.csanchez.jenkins.plugins.kubernetes.pipeline.ContainerExecDecorator$1] At ContainerExecDecorator.java:[line 248] VA_FORMAT_STRING_USES_NEWLINE

@scoheb
Copy link
Contributor

scoheb commented Oct 16, 2017

@marvinthepa were you also able to reproduce the "-1" script return error?

@marvinthepa
Copy link
Contributor

@carlossg: findbugs error fixed, good to merge.

@carlossg carlossg merged commit ecb8779 into jenkinsci:master Oct 16, 2017
@MattLud MattLud deleted the JENKINS-46670 branch October 16, 2017 16:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants