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

Add speedscope remote viewer #33

Merged
merged 2 commits into from
Sep 16, 2021
Merged

Add speedscope remote viewer #33

merged 2 commits into from
Sep 16, 2021

Conversation

gmcgibbon
Copy link
Member

@gmcgibbon gmcgibbon commented Jul 20, 2021

Adds remote speedscope viewer for profiling on remote hosts. It uses rack middleware to hijack /app_profiler requests to list and view profiles of a booted rack / rails app. Very similar to what rack mini profiler does.

The routes are:

/app_profiler to list profiles
/app_profiler/{id} to show a specific profile in speedscope
/app_profiler/viewer/index.html to mount speedscope via Rack::File

@gmcgibbon gmcgibbon force-pushed the view_middleware branch 2 times, most recently from 54b47dd to 19ad44a Compare July 20, 2021 18:09
@gmcgibbon gmcgibbon requested a review from a team July 26, 2021 21:20
Copy link
Member

@dalehamel dalehamel left a comment

Choose a reason for hiding this comment

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

Code itself looks fine, but I feel like this gem overall is starting to do too much, as it is now trying to cover both production and development profiling use-cases.

It would be nice if there was a safety to prevent trying to use this remove viewer if the app is in production, right now it relies on users installing the middleware for development only.

I'd like to also ask @Shopify/appsec review this since rails application security isn't my forte.

<title>App Profiler</title>
</head>
<body>
#{html}
Copy link

Choose a reason for hiding this comment

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

I believe rendering like this leaves you vulnerable to XSS if html ever had anything malicious in it since ActiveSupport provides the default sanitizations

Copy link
Member Author

Choose a reason for hiding this comment

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

sanitize belongs to Action View which IIRC uses https://github.com/rails/rails-html-sanitizer. I think I can use that here.

iframe.style.width = '100vw';
iframe.style.height = '100vh';
iframe.style.border = 'none';
iframe.setAttribute('src', '/app_profiler/viewer/index.html#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{name}');
Copy link

Choose a reason for hiding this comment

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

similarly here - not sure if name can / will ever be user controlled, but this looks like it would lead to XSS

Copy link
Member Author

Choose a reason for hiding this comment

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

The name would be the match of \A/app_profiler/(.*)\z in the path as long as it maps to a file's basename without extension (eg. tmp/app_profiler/my_profile-4.json => my_profile-4) . Since the filename is sanitized I don't think we need to worry, but I can remove the interpolation and make the title generalized instead if you'd like.

Choose a reason for hiding this comment

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

Where does this sanitization/creation of this filename happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

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

Thanks!

Comment on lines 9 to 22
def yarn(command)
setup_yarn unless yarn_setup
exec("yarn #{command}") do
raise YarnError, "Failed to run #{command}."
end
end
Copy link

Choose a reason for hiding this comment

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

This seems very dangerous. Would highly recommend we not just accept any command pass it to yarn. I don't know it well enough but if there's any flag that yarn takes that could result in arbitrary read/write, it would be bad. Would recommend instead we allow-list the commands or actual write the methods with the allowed yarn commands we'd accept

Copy link
Member Author

Choose a reason for hiding this comment

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

This was already in the codebase, I just moved it. Will address in a separate commit.

Comment on lines 40 to 66
def exec(command)
system(command).tap do |return_code|
yield unless return_code
end
Copy link

Choose a reason for hiding this comment

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

Similarly, this seems needlessly dangerous. Instead of accepting any command and passing it to system, we should allow list what we want to run or just write individual methods with no variability

Copy link
Member Author

Choose a reason for hiding this comment

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

It is a private method and the only consumer is yarn. I can inline it if that would make sense to you?

Copy link
Member

@dalehamel dalehamel left a comment

Choose a reason for hiding this comment

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

pending appsec feedback

@gmcgibbon gmcgibbon force-pushed the view_middleware branch 3 times, most recently from eb43259 to a70273d Compare August 16, 2021 21:36
@gmcgibbon gmcgibbon removed the request for review from yaworsk September 14, 2021 21:02
Copy link

@NicholasEllul NicholasEllul left a comment

Choose a reason for hiding this comment

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

Left a comment to help add more proactive security around the call to system(...). Pete mentioned, the most secure way to use the system(...) function is to have an allowlist of things that it can do. You do a great job of this with the ensure_command_valid? command, and now the only remaining place of ambiguity is in the options parameter.

From what I can see, yarn init only gets called with the --yes option, and yarn add only gets called with the speedscope option followed by either the file path, or --dev --ignore-workspace-root-check. Narrowing the scope of potential inputs and enforcing devs working on this in the future to be explicit about what they intend to allow will minimize the risk of something slipping between the cracks into this powerful method

Comment on lines 47 to 67
def exec(*command)
system(*command).tap do |return_code|
yield unless return_code
end
end

Choose a reason for hiding this comment

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

Since we would never want to run exec without calling ensure_command_valid? can we move the call to that function here? That way down the road if exec gets reused somewhere, we can trust that it will always have its safeguards for checking the command bundled into it.

Copy link
Member Author

@gmcgibbon gmcgibbon Sep 15, 2021

Choose a reason for hiding this comment

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

I changed it to permit all command strings. This should be the most secure option despite the cost of iteration and matching. I can instead hardcode these strings into the methods and forgo allow-listing, but I don't know how you feel about that.

Copy link

@NicholasEllul NicholasEllul Sep 16, 2021

Choose a reason for hiding this comment

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

hardcode these strings into the methods and forgo allow-listing

Can you expand on what you mean by this? What you've done here is fantastic with the allow list, if we can maintain a similar strict list of commands another way that you feel is more efficient i'm ok with that as well 👍

Copy link
Member Author

@gmcgibbon gmcgibbon Sep 16, 2021

Choose a reason for hiding this comment

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

Sorry, I meant multiple allow-lists. I was originally permitting specific subcommands to yarn, and then adding another list for exec, but permitting commands + options once in exec simplified things and sidestepped allowing invalid things like yarn init speedscope --dev.

@gmcgibbon gmcgibbon force-pushed the view_middleware branch 2 times, most recently from ae4efd6 to fd5cd2e Compare September 15, 2021 22:24
gmcgibbon and others added 2 commits September 15, 2021 18:25
Co-authored-by: Pragya Daga <pragya.daga@shopify.com>
Copy link

@NicholasEllul NicholasEllul left a comment

Choose a reason for hiding this comment

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

Left a comment, but i'm happy with the allow list that was added! Re-request a review if theres any changes to that you need me to look over

@gmcgibbon gmcgibbon merged commit f8746b0 into main Sep 16, 2021
@gmcgibbon gmcgibbon deleted the view_middleware branch September 16, 2021 19:28
@shopify-shipit shopify-shipit bot temporarily deployed to production March 2, 2022 15:30 Inactive
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