Skip to content

Incomplete url string sanitization#8354

Merged
aibaars merged 11 commits intogithub:mainfrom
aibaars:incomplete-url-string-sanitization
Mar 31, 2022
Merged

Incomplete url string sanitization#8354
aibaars merged 11 commits intogithub:mainfrom
aibaars:incomplete-url-string-sanitization

Conversation

@aibaars
Copy link
Contributor

@aibaars aibaars commented Mar 7, 2022

Added a new query, rb/incomplete-url-substring-sanitization. The query finds instances where a URL is incompletely sanitized due to insufficient checks.

@aibaars aibaars force-pushed the incomplete-url-string-sanitization branch 15 times, most recently from 5d9ee09 to 49b4fe7 Compare March 8, 2022 09:47
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github github deleted a comment from github-actions bot Mar 8, 2022
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2022

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

@aibaars aibaars marked this pull request as ready for review March 8, 2022 16:41
@aibaars aibaars requested review from a team as code owners March 8, 2022 16:41
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2022

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

erik-krogh
erik-krogh previously approved these changes Mar 9, 2022
Copy link
Contributor

@erik-krogh erik-krogh left a comment

Choose a reason for hiding this comment

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

JS 👍

}

/**
* A check of form `A.index(B) != -1`, `A.index(B) >= 0`, or similar.
Copy link
Contributor

Choose a reason for hiding this comment

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

String#index and Array#index return nil on failure, not -1.

Perhaps we should extend this to cover checks of the form A.index(B) != nil and !A.index(B).nil?.

or
value = -1 and polarity = true and comparison.getExpr() instanceof NEExpr
or
value = 0 and polarity = false and comparison.getExpr() instanceof NEExpr
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this case? I'd say that value = 0 is only relevant for relational comparisons.

* A.index(B) >= 0
* ```
*/
class InclusionTest extends DataFlow::Node instanceof InclusionTest::Range {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this using data-flow nodes instead of CFG nodes?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The JavaScript library does the same, so keeping it this way makes it easier to share the implementation of the query.

Co-authored-by: Nick Rolfe <nickrolfe@github.com>
@github-actions
Copy link
Contributor

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
        end
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

@github-actions
Copy link
Contributor

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
        end
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

…ng-sanitization

Conflicts:
	config/identical-files.json
	javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.ql
	javascript/ql/src/Security/CWE-020/IncompleteUrlSubstringSanitization.qll
	ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qll
@github-actions
Copy link
Contributor

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
        end
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

@github-actions
Copy link
Contributor

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
        end
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

(
value = index.getConstantValue().getInt() and value = 0
or
index.getExpr() instanceof NilLiteral and value = -1
Copy link
Contributor

Choose a reason for hiding this comment

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

Can use index.getConstantValue().isNil() instead.

*/
class InclusionTest extends DataFlow::Node instanceof InclusionTest::Range {
/** Gets the `A` in `A.include?(B)`. */
DataFlow::Node getContainerNode() { result = super.getContainerNode() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final

DataFlow::Node getContainerNode() { result = super.getContainerNode() }

/** Gets the `B` in `A.include?(B)`. */
DataFlow::Node getContainedNode() { result = super.getContainedNode() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final

* If the polarity is `false` the check returns `true` if the container does not contain
* the given element.
*/
boolean getPolarity() { result = super.getPolarity() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final

private class Includes_Native extends Range, DataFlow::CallNode {
Includes_Native() {
this.getMethodName() = "include?" and
count(this.getArgument(_)) = 1
Copy link
Contributor

Choose a reason for hiding this comment

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

may as well use strictcount

exists(ExprNodes::ComparisonOperationCfgNode comparison |
this.asExpr() = comparison and
indexOf.getMethodName() = "index" and
count(indexOf.getArgument(_)) = 1 and
Copy link
Contributor

Choose a reason for hiding this comment

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

strictcount

/**
* Gets the `A` in `A.start_with?(B)`.
*/
DataFlow::Node getBaseString() { result = super.getBaseString() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final

/**
* Gets the `B` in `A.start_with?(B)`.
*/
DataFlow::Node getSubstring() { result = super.getSubstring() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final

* If the polarity is `false` the check returns `true` if the string does not end
* with the given substring.
*/
boolean getPolarity() { result = super.getPolarity() }
Copy link
Contributor

Choose a reason for hiding this comment

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

final


/** Holds if `node` may evaluate to `value` */
predicate mayHaveStringValue(DataFlow::Node node, string value) {
node.asExpr().getExpr().getConstantValue().getString() = value
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove getExpr()

@github-actions
Copy link
Contributor

QHelp previews:

ruby/ql/src/queries/security/cwe-020/IncompleteUrlSubstringSanitization.qhelp

Incomplete URL substring sanitization

Sanitizing untrusted URLs is an important technique for preventing attacks such as request forgeries and malicious redirections. Usually, this is done by checking that the host of a URL is in a set of allowed hosts.

However, treating the URL as a string and checking if one of the allowed hosts is a substring of the URL is very prone to errors. Malicious URLs can bypass such security checks by embedding one of the allowed hosts in an unexpected location.

Even if the substring check is not used in a security-critical context, the incomplete check may still cause undesirable behaviors when the check succeeds accidentally.

Recommendation

Parse a URL before performing a check on its host value, and ensure that the check handles arbitrary subdomain sequences correctly.

Example

The following example code checks that a URL redirection will reach the example.com domain, or one of its subdomains, and not some malicious site.

class AppController < ApplicationController
    def index
        url = params[:url]
        # BAD: the host of `url` may be controlled by an attacker
        if url.include?("example.com")
            redirect_to url
        end
    end
end

The substring check is, however, easy to bypass. For example by embedding example.com in the path component: http://evil-example.net/example.com, or in the query string component: http://evil-example.net/?x=example.com. Address these shortcomings by checking the host of the parsed URL instead:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # BAD: the host of `url` may be controlled by an attacker
        if host.include?("example.com")
            redirect_to url
        end
    end
end

This is still not a sufficient check as the following URLs bypass it: http://evil-example.com http://example.com.evil-example.net. Instead, use an explicit whitelist of allowed hosts to make the redirect secure:

class AppController < ApplicationController
    def index
        url = params[:url]
        host = URI(url).host
        # GOOD: the host of `url` can not be controlled by an attacker
        allowedHosts = [
            'example.com',
            'beta.example.com',
            'www.example.com'
        ]
        if allowedHosts.include?(host)
            redirect_to url
        end
    end
end

References

@aibaars aibaars merged commit 15c54f6 into github:main Mar 31, 2022
@hvitved hvitved mentioned this pull request Mar 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants