Adjust password manager polyfill for form injections (#17830)

This commit is contained in:
Steve Repsher 2023-09-19 08:51:38 -04:00 committed by GitHub
parent 713ebfcc22
commit e46f2cd9bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -35,20 +35,47 @@ export class HaPasswordManagerPolyfill extends LitElement {
super.connectedCallback(); super.connectedCallback();
this._styleElement = document.createElement("style"); this._styleElement = document.createElement("style");
this._styleElement.textContent = css` this._styleElement.textContent = css`
/* Polyfill form is sized and vertically aligned with true form, then positioned offscreen
rather than hiding so it does not create a new stacking context */
.password-manager-polyfill { .password-manager-polyfill {
position: absolute; position: absolute;
opacity: 0; box-sizing: border-box;
z-index: -1;
} }
.password-manager-polyfill input { /* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */
.password-manager-polyfill > *:not(.wrapper),
.password-manager-polyfill > .wrapper > * {
position: relative;
left: 10000px;
}
/* Size and hide our polyfill fields */
.password-manager-polyfill .underneath {
display: block;
box-sizing: border-box;
width: 100%; width: 100%;
height: 62px; padding: 0 16px;
padding: 0;
border: 0; border: 0;
z-index: -1;
height: 21px;
/* Transparency is only needed to hide during paint or in case of misalignment,
but LastPass will fail if it's 0, so we use 1% */
opacity: 0.01;
} }
.password-manager-polyfill input[type="submit"] { .password-manager-polyfill input.underneath {
width: 0; height: 28px;
height: 0; margin-bottom: 30.5px;
}
/* Button position is not important, but size should not be zero */
.password-manager-polyfill > input.underneath[type="submit"] {
width: 1px;
height: 1px;
margin: 0 auto;
overflow: hidden;
}
/* Ensure injected elements will be on top */
.password-manager-polyfill > *:not(.underneath, .wrapper),
.password-manager-polyfill > .wrapper > *:not(.underneath) {
isolation: isolate;
z-index: auto;
} }
`.toString(); `.toString();
document.head.append(this._styleElement); document.head.append(this._styleElement);
@ -77,16 +104,25 @@ export class HaPasswordManagerPolyfill extends LitElement {
class="password-manager-polyfill" class="password-manager-polyfill"
style=${styleMap({ style=${styleMap({
top: `${this.boundingRect?.y || 148}px`, top: `${this.boundingRect?.y || 148}px`,
left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`, left: `calc(50% - ${
(this.boundingRect?.width || 360) / 2
}px - 10000px)`,
width: `${this.boundingRect?.width || 360}px`, width: `${this.boundingRect?.width || 360}px`,
})} })}
aria-hidden="true" action="/auth"
method="post"
@submit=${this._handleSubmit} @submit=${this._handleSubmit}
> >
${autocompleteLoginFields(this.step.data_schema).map((input) => ${autocompleteLoginFields(this.step.data_schema).map((input) =>
this.render_input(input) this.render_input(input)
)} )}
<input type="submit" /> <input
type="submit"
value="Login"
class="underneath"
tabindex="-2"
aria-hidden="true"
/>
</form> </form>
`; `;
} }
@ -99,26 +135,35 @@ export class HaPasswordManagerPolyfill extends LitElement {
return ""; return "";
} }
return html` return html`
<input <!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) -->
tabindex="-1" <label for=${schema.name} class="underneath" aria-hidden="true">
.id=${schema.name} ${schema.name}
.name=${schema.name} </label>
.type=${inputType} <!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead -->
.value=${this.stepData[schema.name] || ""} <div class="wrapper" aria-hidden="true">
.autocomplete=${schema.autocomplete} <!-- LastPass fails with tabindex of -1, so we trick with -2 -->
@input=${this._valueChanged} <input
@change=${this._valueChanged} class="underneath"
/> tabindex="-2"
.id=${schema.name}
.name=${schema.name}
.type=${inputType}
.value=${this.stepData[schema.name] || ""}
.autocomplete=${schema.autocomplete}
@input=${this._valueChanged}
@change=${this._valueChanged}
/>
</div>
`; `;
} }
private _handleSubmit(ev: Event) { private _handleSubmit(ev: SubmitEvent) {
ev.preventDefault(); ev.preventDefault();
fireEvent(this, "form-submitted"); fireEvent(this, "form-submitted");
} }
private _valueChanged(ev: Event) { private _valueChanged(ev: Event) {
const target = ev.target! as HTMLInputElement; const target = ev.target as HTMLInputElement;
this.stepData = { ...this.stepData, [target.id]: target.value }; this.stepData = { ...this.stepData, [target.id]: target.value };
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: this.stepData, value: this.stepData,