-
-
Notifications
You must be signed in to change notification settings - Fork 352
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(core) Fix attachment and hyperlink vulnerabilities
Summary: Attachments were prone to XSS-based attacks if attachments injected with scripts were previewed or opened. This is now addressed by CSP. Hyperlink cells were prone to similar attacks if `javascript:...` URLs were inserted into cells. This has also been addressed by sanitizing URLs. Thank you to Florent <[email protected]> and Grégoire Cutzach <[email protected]> for reporting and co-authoring these changes. Co-authored-by: Florent <[email protected]> Co-authored-by: Grégoire Cutzach <[email protected]> Test Plan: Browser and unit tests. Reviewers: dsagal, paulfitz Reviewed By: dsagal, paulfitz Subscribers: dsagal, paulfitz, fflorent Differential Revision: https://phab.getgrist.com/D4413
- Loading branch information
1 parent
6996ea3
commit a792bdc
Showing
14 changed files
with
956 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
import DOMPurify from "dompurify"; | ||
|
||
// Export dependencies for stubbing in tests. | ||
export const Deps = { DOMPurify }; | ||
|
||
/** | ||
* Returns the provided URL if it is valid and safe to use in | ||
* HTTP-only contexts, such as form redirects and custom widget | ||
* URLs. | ||
* | ||
* Returns `null` if the URL is invalid or unsafe. | ||
* | ||
* For sanitizing hyperlink URLs, such as those used by `a` | ||
* elements, see `sanitizeLinkUrl`. | ||
*/ | ||
export function sanitizeHttpUrl(url: string): string | null { | ||
try { | ||
const parsedUrl = new URL(url); | ||
if (!["http:", "https:"].includes(parsedUrl.protocol)) { | ||
return null; | ||
} | ||
|
||
return parsedUrl.href; | ||
} catch (e) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Returns the provided URL if it is valid and safe to use for hyperlinks, | ||
* such as those used by `a` elements. This includes URLs prefixed with | ||
* `http[s]:`, `mailto:`, and `tel:`, and excludes URLs prefixed with | ||
* `javascript:`. | ||
* | ||
* Returns `null` if the URL is invalid or unsafe. | ||
* | ||
* For sanitizing HTTP-only URLs, such as those used for redirects, see | ||
* `sanitizeHttpUrl`. | ||
*/ | ||
export function sanitizeLinkUrl(url: string): string | null { | ||
return Deps.DOMPurify.isValidAttribute("a", "href", url) ? url : null; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import { | ||
Deps, | ||
sanitizeHttpUrl, | ||
sanitizeLinkUrl, | ||
} from "app/client/lib/sanitizeUrl"; | ||
import { assert } from "chai"; | ||
import DOMPurify from "dompurify"; | ||
import { JSDOM } from "jsdom"; | ||
import * as sinon from "sinon"; | ||
|
||
describe("sanitizeUrl", function () { | ||
let sandbox: sinon.SinonSandbox; | ||
|
||
beforeEach(function () { | ||
// These grainjs browserGlobals are needed for using dom() in tests. | ||
const jsdomDoc = new JSDOM("<!doctype html><html><body></body></html>"); | ||
sandbox = sinon.createSandbox(); | ||
sandbox.stub(Deps, "DOMPurify").value(DOMPurify(jsdomDoc.window)); | ||
}); | ||
|
||
afterEach(function () { | ||
sandbox.restore(); | ||
}); | ||
|
||
describe("sanitizeHttpUrl", function () { | ||
it("returns the provided URL if valid", function () { | ||
assert.equal( | ||
sanitizeHttpUrl("https://example.com"), | ||
"https://example.com/" | ||
); | ||
assert.equal( | ||
sanitizeHttpUrl("http://example.com"), | ||
"http://example.com/" | ||
); | ||
}); | ||
|
||
it("returns null if the provided URL is invalid", function () { | ||
assert.isNull(sanitizeHttpUrl("www.example.com")); | ||
assert.isNull(sanitizeHttpUrl("")); | ||
assert.isNull(sanitizeHttpUrl("invalid")); | ||
assert.isNull(sanitizeHttpUrl("mailto:[email protected]")); | ||
assert.isNull(sanitizeHttpUrl("ftp://getgrist.com/path")); | ||
assert.isNull(sanitizeHttpUrl("javascript:alert()")); | ||
}); | ||
}); | ||
|
||
describe("sanitizeLinkUrl", function () { | ||
it("returns the provided URL if valid", function () { | ||
assert.equal( | ||
sanitizeLinkUrl("https://example.com"), | ||
"https://example.com" | ||
); | ||
assert.equal(sanitizeLinkUrl("http://example.com"), "http://example.com"); | ||
assert.equal(sanitizeLinkUrl("www.example.com"), "www.example.com"); | ||
assert.equal(sanitizeLinkUrl(""), ""); | ||
assert.equal( | ||
sanitizeLinkUrl("mailto:[email protected]"), | ||
"mailto:[email protected]" | ||
); | ||
assert.equal(sanitizeLinkUrl("tel:0123456789"), "tel:0123456789"); | ||
assert.equal( | ||
sanitizeLinkUrl("ftp://getgrist.com/path"), | ||
"ftp://getgrist.com/path" | ||
); | ||
}); | ||
|
||
it("returns null if the provided URL is unsafe", function () { | ||
assert.isNull(sanitizeLinkUrl("javascript:alert()")); | ||
}); | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.