SECURITY: Expand and improve SSRF Protections (#18815)

See https://github.com/discourse/discourse/security/advisories/GHSA-rcc5-28r3-23rr

Co-authored-by: OsamaSayegh <asooomaasoooma90@gmail.com>
Co-authored-by: Daniel Waterworth <me@danielwaterworth.com>
This commit is contained in:
David Taylor
2022-11-01 16:33:17 +00:00
committed by GitHub
parent 695b44269b
commit 68b4fe4cf8
42 changed files with 1164 additions and 443 deletions

View File

@@ -3,8 +3,6 @@ import EmberObject from "@ember/object";
import I18n from "I18n";
import { alias } from "@ember/object/computed";
import discourseComputed from "discourse-common/utils/decorators";
import { extractDomainFromUrl } from "discourse/lib/utilities";
import { isAbsoluteURL } from "discourse-common/lib/get-url";
import { isEmpty } from "@ember/utils";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
@@ -89,38 +87,20 @@ export default Controller.extend({
actions: {
save() {
this.set("saved", false);
const url = this.get("model.payload_url");
const domain = extractDomainFromUrl(url);
const model = this.model;
const isNew = model.get("isNew");
const saveWebHook = () => {
return model
.save()
.then(() => {
this.set("saved", true);
this.adminWebHooks.get("model").addObject(model);
return model
.save()
.then(() => {
this.set("saved", true);
this.adminWebHooks.get("model").addObject(model);
if (isNew) {
this.transitionToRoute("adminWebHooks.show", model.get("id"));
}
})
.catch(popupAjaxError);
};
if (
domain === "localhost" ||
domain.match(/192\.168\.\d+\.\d+/) ||
domain.match(/127\.\d+\.\d+\.\d+/) ||
isAbsoluteURL(url)
) {
return this.dialog.yesNoConfirm({
message: I18n.t("admin.web_hooks.warn_local_payload_url"),
didConfirm: () => saveWebHook(),
});
}
return saveWebHook();
if (isNew) {
this.transitionToRoute("adminWebHooks.show", model.get("id"));
}
})
.catch(popupAjaxError);
},
destroy() {

View File

@@ -31,6 +31,7 @@ export default Controller.extend(ModalFunctionality, {
advancedVisible: false,
selectedType: alias("themesController.currentTab"),
component: equal("selectedType", COMPONENTS),
urlPlaceholder: "https://github.com/discourse/sample_theme",
init() {
this._super(...arguments);
@@ -79,29 +80,6 @@ export default Controller.extend(ModalFunctionality, {
);
},
@discourseComputed("privateChecked")
urlPlaceholder(privateChecked) {
return privateChecked
? "git@github.com:discourse/sample_theme.git"
: "https://github.com/discourse/sample_theme";
},
@observes("privateChecked")
privateWasChecked() {
const checked = this.privateChecked;
if (checked && !this._keyLoading) {
this._keyLoading = true;
ajax(this.keyGenUrl, { type: "POST" })
.then((pair) => {
this.set("publicKey", pair.public_key);
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
@discourseComputed("name")
nameTooShort(name) {
return !name || name.length < MIN_NAME_LENGTH;
@@ -116,6 +94,22 @@ export default Controller.extend(ModalFunctionality, {
}
},
@observes("checkPrivate")
privateWasChecked() {
const checked = this.checkPrivate;
if (checked && !this._keyLoading && !this.publicKey) {
this._keyLoading = true;
ajax(this.keyGenUrl, { type: "POST" })
.then((pair) => {
this.set("publicKey", pair.public_key);
})
.catch(popupAjaxError)
.finally(() => {
this._keyLoading = false;
});
}
},
@discourseComputed("selection", "themeCannotBeInstalled")
submitLabel(selection, themeCannotBeInstalled) {
if (themeCannotBeInstalled) {
@@ -127,15 +121,14 @@ export default Controller.extend(ModalFunctionality, {
}`;
},
@discourseComputed("privateChecked", "checkPrivate", "publicKey")
showPublicKey(privateChecked, checkPrivate, publicKey) {
return privateChecked && checkPrivate && publicKey;
@discourseComputed("checkPrivate", "publicKey")
showPublicKey(checkPrivate, publicKey) {
return checkPrivate && publicKey;
},
onClose() {
this.setProperties({
duplicateRemoteThemeWarning: null,
privateChecked: false,
localFile: null,
uploadUrl: null,
publicKey: null,
@@ -209,11 +202,8 @@ export default Controller.extend(ModalFunctionality, {
options.data = {
remote: this.uploadUrl,
branch: this.branch,
public_key: this.publicKey,
};
if (this.privateChecked) {
options.data.public_key = this.publicKey;
}
}
// User knows that theme cannot be installed, but they want to continue

View File

@@ -61,25 +61,15 @@
<div class="label">{{i18n "admin.customize.theme.remote_branch"}}</div>
<Input @value={{this.branch}} placeholder="main" />
</div>
{{/if}}
<div class="check-private">
<label>
<Input @type="checkbox" @checked={{this.privateChecked}} />
{{i18n "admin.customize.theme.is_private"}}
</label>
</div>
{{#if this.showPublicKey}}
<div class="public-key">
<div class="label">{{i18n "admin.customize.theme.public_key"}}</div>
<div class="public-key-text-wrapper">
<Textarea class="public-key-value" readonly={{true}} @value={{this.publicKey}} /> <CopyButton @selector="textarea.public-key-value" />
</div>
{{#if this.showPublicKey}}
<div class="public-key">
<div class="label">{{i18n "admin.customize.theme.public_key"}}</div>
<div class="public-key-text-wrapper">
<Textarea class="public-key-value" readonly={{true}} @value={{this.publicKey}} /> <CopyButton @selector="textarea.public-key-value" />
</div>
{{else}}
{{#if this.privateChecked}}
<div class="public-key-note">{{i18n "admin.customize.theme.public_key_note"}}</div>
{{/if}}
{{/if}}
</div>
{{/if}}
</div>
{{/if}}