diff --git a/src/core/components/auth/oauth2.jsx b/src/core/components/auth/oauth2.jsx index a4cb6a79..a480857e 100644 --- a/src/core/components/auth/oauth2.jsx +++ b/src/core/components/auth/oauth2.jsx @@ -96,6 +96,7 @@ export default class Oauth2 extends React.Component { const AuthError = getComponent("authError") const JumpToPath = getComponent("JumpToPath", true) const Markdown = getComponent( "Markdown" ) + const InitializedInput = getComponent("InitializedInput") const { isOAS3 } = specSelectors @@ -170,10 +171,10 @@ export default class Oauth2 extends React.Component { { isAuthorized ? ****** : - @@ -187,8 +188,8 @@ export default class Oauth2 extends React.Component { { isAuthorized ? ****** : - diff --git a/src/core/components/initialized-input.jsx b/src/core/components/initialized-input.jsx new file mode 100644 index 00000000..2f4189c2 --- /dev/null +++ b/src/core/components/initialized-input.jsx @@ -0,0 +1,36 @@ +// This component provides an interface that feels like an uncontrolled input +// to consumers, while providing a `defaultValue` interface that initializes +// the input's value using JavaScript value property APIs instead of React's +// vanilla[0] implementation that uses HTML value attributes. +// +// This is useful in situations where we don't want to surface an input's value +// into the HTML/CSS-exposed side of the DOM, for example to avoid sequential +// input chaining attacks[1]. +// +// [0]: https://github.com/facebook/react/blob/baff5cc2f69d30589a5dc65b089e47765437294b/fixtures/dom/src/components/fixtures/text-inputs/README.md +// [1]: https://github.com/d0nutptr/sic + +import React from "react" +import PropTypes from "prop-types" + +export default class InitializedInput extends React.Component { + componentDidMount() { + // Set the element's `value` property (*not* the `value` attribute) + // once, on mount, if an `initialValue` is provided. + if(this.props.initialValue) { + this.inputRef.value = this.props.initialValue + } + } + + render() { + // Filter out `value` and `defaultValue`, since we have our own + // `initialValue` interface that we provide. + // eslint-disable-next-line no-unused-vars, react/prop-types + const { value, defaultValue, ...otherProps } = this.props + return this.inputRef = c} /> + } +} + +InitializedInput.propTypes = { + initialValue: PropTypes.string +} diff --git a/src/core/components/providers/markdown.jsx b/src/core/components/providers/markdown.jsx index d8d31a97..d3a2e482 100644 --- a/src/core/components/providers/markdown.jsx +++ b/src/core/components/providers/markdown.jsx @@ -51,6 +51,7 @@ export default Markdown export function sanitizer(str) { return DomPurify.sanitize(str, { - ADD_ATTR: ["target"] + ADD_ATTR: ["target"], + FORBID_TAGS: ["style"], }) } diff --git a/src/core/presets/base.js b/src/core/presets/base.js index 015cd80d..6fe8f43c 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -53,6 +53,7 @@ import Headers from "core/components/headers" import Errors from "core/components/errors" import ContentType from "core/components/content-type" import Overview from "core/components/overview" +import InitializedInput from "core/components/initialized-input" import Info, { InfoUrl, InfoBasePath @@ -105,6 +106,7 @@ export default function() { basicAuth: BasicAuth, clear: Clear, liveResponse: LiveResponse, + InitializedInput, info: Info, InfoContainer, JumpToPath, diff --git a/test/e2e-cypress/static/documents/petstore-expanded.openapi.yaml b/test/e2e-cypress/static/documents/petstore-expanded.openapi.yaml index 20bd288b..98646200 100644 --- a/test/e2e-cypress/static/documents/petstore-expanded.openapi.yaml +++ b/test/e2e-cypress/static/documents/petstore-expanded.openapi.yaml @@ -13,6 +13,8 @@ info: url: https://www.apache.org/licenses/LICENSE-2.0.html servers: - url: http://petstore.swagger.io/api +security: + - Petstore: [] paths: /pets: get: @@ -152,4 +154,13 @@ components: type: integer format: int32 message: - type: string \ No newline at end of file + type: string + securitySchemes: + Petstore: + type: oauth2 + flows: + implicit: + authorizationUrl: https://example.com/api/oauth/dialog + scopes: + write:pets: modify pets in your account + read:pets: read your pets diff --git a/test/e2e-cypress/static/documents/security/sequential-import-chaining/injection.css b/test/e2e-cypress/static/documents/security/sequential-import-chaining/injection.css new file mode 100644 index 00000000..edc480f6 --- /dev/null +++ b/test/e2e-cypress/static/documents/security/sequential-import-chaining/injection.css @@ -0,0 +1,7 @@ +* { + color: red !important; /* for humans */ +} + +h4 { + display: none; /* for machines, used to trace whether this sheet is applied */ +} diff --git a/test/e2e-cypress/static/documents/security/sequential-import-chaining/openapi.yaml b/test/e2e-cypress/static/documents/security/sequential-import-chaining/openapi.yaml new file mode 100644 index 00000000..e4e4ade8 --- /dev/null +++ b/test/e2e-cypress/static/documents/security/sequential-import-chaining/openapi.yaml @@ -0,0 +1,10 @@ +openapi: "3.0.0" + +info: + title: Sequential Import Chaining + description: > +

This h4 would be hidden by the injected CSS

+ + This document tests the ability of a ` diff --git a/test/e2e-cypress/static/documents/security/sequential-import-chaining/swagger.yaml b/test/e2e-cypress/static/documents/security/sequential-import-chaining/swagger.yaml new file mode 100644 index 00000000..5f9cc448 --- /dev/null +++ b/test/e2e-cypress/static/documents/security/sequential-import-chaining/swagger.yaml @@ -0,0 +1,10 @@ +swagger: "2.0" + +info: + title: Sequential Import Chaining + description: > +

This h4 would be hidden by the injected CSS

+ + This document tests the ability of a ` diff --git a/test/e2e-cypress/static/documents/xss/oauth2.yaml b/test/e2e-cypress/static/documents/security/xss-oauth2.yaml similarity index 100% rename from test/e2e-cypress/static/documents/xss/oauth2.yaml rename to test/e2e-cypress/static/documents/security/xss-oauth2.yaml diff --git a/test/e2e-cypress/tests/features/xss/oauth2.js b/test/e2e-cypress/tests/security/oauth2.js similarity index 90% rename from test/e2e-cypress/tests/features/xss/oauth2.js rename to test/e2e-cypress/tests/security/oauth2.js index 3d7b727a..4d01ba3c 100644 --- a/test/e2e-cypress/tests/features/xss/oauth2.js +++ b/test/e2e-cypress/tests/security/oauth2.js @@ -1,6 +1,6 @@ describe("XSS: OAuth2 authorizationUrl sanitization", () => { it("should filter out a javascript URL", () => { - cy.visit("/?url=/documents/xss/oauth2.yaml") + cy.visit("/?url=/documents/security/xss-oauth2.yaml") .window() .then(win => { let args = null diff --git a/test/e2e-cypress/tests/security/sequential-import-chaining.js b/test/e2e-cypress/tests/security/sequential-import-chaining.js new file mode 100644 index 00000000..90887def --- /dev/null +++ b/test/e2e-cypress/tests/security/sequential-import-chaining.js @@ -0,0 +1,58 @@ +describe("Security: CSS Sequential Import Chaining", () => { + describe("in OpenAPI 3.0", () => { + describe("CSS Injection via Markdown", () => { + it("should filter