-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
Core: Sync args state to URL #13803
Core: Sync args state to URL #13803
Conversation
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.
I'm unsure about this approach.
We need to also parse args from the URL if you browse directly to the /iframe.html
(this is what folks want to do in tests after all)
Following through from that, we need to have the arg parsing logic in the preview. Possibly we also should set the iframe URL from the preview too (this part is up for debate).
I sort of feel like the manager should perhaps just pass the arg query parameters straight through to the preview by whacking them on the iframe URL (similar to how it does the story id, IIRC) and let the preview map them to actual arg values and ultimately emit the args, to be later captured by the manager.
OTOH I'm not sure who should then serialize said args to the URL.
Imho, this changes will add security issue like XSS. Some components (if not all) doesn't sanitize its properties, because there are used internally. When using args, the user has to interact with Storybook. However, if the properties are automatically inject through the url to the component, it's possible to craft an url to abuse components without the user knowledge. |
@tmeasday You're right. My first draft didn't include any handling for the iframe (i.e. support "ejecting"). I've added that now, and that indeed involved parsing the args parameter in the preview instead of in the manager. The preview will now set the initial args based on its URL, rather than the manager. The manager will pick up that args change and update accordingly.
I don't think we need to do this. When ejecting, the relevant args are added to the iframe URL, so it will parse and use them. When loaded from the manager, the iframe URL already inherits the params from the manager URL, so it will initialize in the same way and send a signal to the manager to update the args based on its URL. When navigating to a different story, the iframe URL will not be updated, but the args are updated by other means (from the manager). The URL is only relevant for the initial page load, not when switching between stories. Of course this means that "open frame in new tab" will not work as expected, but I think this is a reasonable limitation if it means we can avoid additional URL syncing complexity. People should just use the eject (or copy) feature for this purpose. |
@j3rem1e That's correct. We are not doing any kind of sanitization, and args are often passed directly to a component, which may be vulnerable to XSS (that depends on the details of how those props are used). Although I think the risk involved is very small (Storybooks typically run on localhost or a unique subdomain, so stealing a cookie isn't really interesting), we should at least offer some way to secure against this. For one, I think we should offer a way to opt out of this feature. Another thing we could do is include a signed hash in the URL, and verify it before applying the URL args. We could also attempt to sanitize the args, but it's very tricky to get that right. It would be the least impactful thing to do though. What do you think? For some added context: The knobs addon is also vulnerable to this. While it doesn't sync to the URL, it does parse from the URL. |
@j3rem1e can you provide an XSS example, even if it's contrived, to help me understand? |
@ghengeveld I work for a company focused on security, so my first reflex is that default should be secure :-) Let's take a hypothetical example: I have a Timeline component. This component shows a discussion and each message is generated from markdown in my backend. my backend is secured, the generated html is sanitized on the backend, the canal is https, my Timeline doesn't sanitize a 2nd time the messages. It's an internal component and it's not possible to use it without the backend. This component is tested through Storybook. As my controls are auto-generated, I see a "message" property which I can enter anything (and html). but it's not important here, because it is entered by the tester and run in the context of the browser of the tester. With this PR, I can craft an URL which show the timeline with arbitrary html/javascript. I can send this url to anybody, and the script will execute in the context of the target browser. In my case, Storybook run under a subdomain of my production server (jenkins) next to a wiki which documents internals of the application. jenkins and the wiki are secured, of course. However, If I send the crafted URL to a user who has admin access to the jenkins, and this user is logged into jenkins, then I can probably steal his cookie/create an user/etc. If I send a "localhost" url to a dev whose is running storybook, then I can execute scripts in the context of "localhost" which is a secure location by default. I can do a lot of things in this context. |
This means URLs are no-longer "hackable" which I think would be a pity. Maybe we could add this signing feature as an opt-in security measure though? Although if the attacker has access to the SB they could easily construct a signed URL containing arbitrary args too, so I'm not sure it achieves all that much. Probably the right thing is to make the whole feature opt-out (or opt-in).
PS aren't your cookies domain scoped? |
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.
I'd like to see some tests of the initial arg getting set on the preview side:
-
A unit test in the store when you set the args in selection specifier, probably add here: https://github.com/storybookjs/storybook/blob/3fd8b6b39746ea54a5e43eb19333097aa3cb0dce/lib/client-api/src/story_store.test.ts#L761:L761
-
A unit test of the url parsing, here:
describe('getSelectionSpecifierFromPath', () => { -
A fuller integration test that the wiring all works (in the preview at least, its a bit tricky to do preview/manager tests) if you can. We have some in here: https://github.com/storybookjs/storybook/blob/next/lib/core/src/client/preview/start.test.ts but it could be tricky.
-
Potentially a full end-to-end test that one of the example Storybooks
a) renders properly with the args specified in the URL
b) the URL changes when you change the arg.
I actually don't even know how to do 4 ( 😉 ) but I reckon this is the kind of feature that could regress really easily as it relies on stuff across the whole stack, so I think its worth the effort to get a test up.
lib/api/src/modules/url.ts
Outdated
fullAPI.on(SET_STORIES, () => { | ||
const data = fullAPI.getCurrentStoryData(); | ||
const storyArgs = isStory(data) ? data.args : undefined; | ||
initialArgs[data.id] = storyArgs; |
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.
Doesn't SET_CURRENT_STORY
get called for the first story too?
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.
No it doesn't. It only triggers when switching between stories.
lib/api/src/modules/url.ts
Outdated
const data = fullAPI.getCurrentStoryData(); | ||
const args = isStory(data) ? data.args : undefined; | ||
initialArgs[data.id] = initialArgs[data.id] || args; | ||
updateArgsParam({ storyId: data.id, args }); |
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.
Is this line necessary? By definition all the args are "initial", right?
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.
This restores any previously customized args when switching back and forth between stories. So no, they're not always initial.
Of course this assumes we want this behavior of restoring args to a previously customized state rather than resetting to the initial args. I think we do.
lib/api/src/modules/url.ts
Outdated
const argsString = stringifyArgs(customizedArgs); | ||
const argsParam = argsString.length ? `&args=${argsString}` : ''; | ||
queryNavigate(`${fullAPI.getUrlState().path}${argsParam}`, { replace: true }); | ||
api.setQueryParams({ args: argsString }); |
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.
Should we checking something's changed before calling replaceState
?
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.
I don't think that's worthwhile. Probably the browser already optimizes for that.
lib/core/src/client/preview/url.ts
Outdated
@@ -60,8 +60,16 @@ const deprecatedLegacyQuery = deprecate( | |||
Use \`id=$storyId\` instead. | |||
See https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#new-url-structure` | |||
); | |||
export const getSelectionSpecifierFromPath: () => StoreSelectionSpecifier = () => { | |||
|
|||
// Lifted from @storybook/router utils |
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.
We can't import? 🤔
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.
No, we have no shared code between manager and preview right now (core-events
being the sole exception, but thats just a bunch of constants). Norbert and Michael are working on something which will introduced a core-common
package, but until then it's easier to copy/paste.
I jumped the gun here again didn't I @ghengeveld? (Noticed this PR is still a draft). Still hopefully the comments above are helpeful.
Yeah, I'm already not sure how this works. Do we even update the URL with the id when changing story anyway? |
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.
Wonderful!!! 😻
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.
Looks good to me, it's a great feature.
We're not quite there yet. The recent change to initialize the story args based on the URL in the story store changed the sequence of steps and is causing the URL to get stripped of other args whenever you change one arg. |
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.
Code's looking good to me
The recent change to initialize the story args based on the URL in the story store changed the sequence of steps and is causing the URL to get stripped of other args whenever you change one arg.
Can we encode this problem in a test then? Would be good not to regress later.
I think the unit + integration tests are sufficient to cover all the logic, and if it breaks regardless it'll be pretty apparent during use.
If you are happy the manager-side tests guarantee args will make it to the iframe and the URL will update correctly (perhaps see above), then I think it's good.
Is the problem here that the manager has no way to tell that the args of the story aren't the original args? Perhaps we need to get the timing of things just right so that the |
Warn when attempting to serialize or unserialize args with disallowed characters.
Did a bunch more work on this. Still need to apply throttling because we're going a lot of work to serialize args. |
There's two commits that can be reverted when the next version of Currently, qs does not support the |
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.
Merging!
Issue: #12291
What I did
This synchronizes args state with the URL.
How to test
If your answer is yes to any of these, please make sure to include it in your PR.