それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

iframeを操作してDOMを制御。iframe制限を超えて金融機関の「お知らせ」を全チェックする

sbi証券のお知らせをためすぎた

SBI証券のお知らせを、200件近く溜め込んでしまった。

SBI証券はログイン後にリダイレクトが走る

リダイレクト画面でのログイン

ログイン時に、もとのリクエストアドレスにリダイレクトされる

たとえば、特定の株価を見ていて、ログインして詳細を続けてみる

たとえば、SBIラップ口座を開いて、ログインして、ラップ口座の詳細を見る。

これが、「リダイレクトされる」ログイン画面

「お知らせ」があるとリダイレクトされない。

お知らせがあると、元いたページにリダイレクトされずに、「お知らせ一覧」に遷移してしまう。

SBIラップ口座でログインしているのに、SBIのお知らせ一覧に行ってしまうのだ。

そもそも、多くの人は「お知らせ一覧」が出てくるのが「正常状態」とすら思ってる。そのように誤解している人も多い。

お知らせ一覧が強制表示される例

お知らせを全部確認しようとしたが、しんどすぎた

SBIのお知らせは数年(2年)分が溜め込めれるので、お知らせを全部確認するのは非常につらい仕事になる。

自動化するか!

さすがに2年前のものはもういらないし、パスワード変更のお知らせ、何回あるんだよ。

私は、書面交付に設定してるので、本当に重要なものは、郵送で送られてくる。

ということを踏まえて自動化するか!

iframe を使う。

金融機関のウェブサイトは、iframe でページを読み込むと、JSでフレーム検出が走る。なのでiframeで読み込めないと思っていた。*1

しかし、iframeは進化している。iframeにはいまや、安全なSANDBOX機能がある。

iframeは「Javascriptオフ」できるのだ。JSをOffにすると、JSによるフレーム検出は無視できるのだ。

もしフレーム化を拒否したければJSはもうだめなのだ。HTTPヘッダでやるべきなのだ。*2

iframe に埋め込む。

お知らせ一覧をiframe に埋め込む

埋め込みさえできれば、DOMアクセスして、全自動できる。

埋め込むためのスクリプト

class iframeLoader {
  constructor() {
    this.iframe = null;
    this.selector = '#MAINAREA01';
  }
  async create_iframe(selector,href) {
    let iframe = null
    let iframe_id = 'my_worker_iframe'
    if (document.getElementById(iframe_id)) {
      iframe = document.getElementById(iframe_id)
    }
    else {
      // init 
      iframe = document.createElement('iframe')
      iframe.setAttribute('sandbox', 'allow-forms allow-same-origin allow-top-navigation')
      iframe.setAttribute('id', 'my_worker_iframe');
      iframe.setAttribute('width', '100%')
      iframe.setAttribute('height', '400px')
      let ele = document.querySelector(selector)
      let first = ele.firstElementChild
      ele.insertBefore(iframe, first)
    }
    return new Promise((resolve, reject) => {
      if (iframe) {
        resolve(iframe)
      }
    })
  }
  async load_iframe(href) {
    this.iframe.src = href
    return new Promise((resolve, reject) => {
      this.iframe.onload = function () { resolve(this.iframe.contentWindow.document) }
    })
  }

  async load_notice_iframe() {
    let ret = $x('//a[contains(./text() , "重要なお知らせ")]');
    let a = ret[0]
    let b = a.onclick.toString().match(/openMsgBox\('(.+)'\)/)[1]
    let href = new URL(b, window.location.href).toString()
    this.iframe = await this.create_iframe(this.selector)
    let doc = await this.load_iframe(href)
    return new Promise((resolve, reject) => {
      resolve(this.iframe.contentWindow.document)
    })
  }

}
class NoticeClicker {
  constructor() {
    this.iframe_id = 'my_worker_iframe';
  }
  get iframe() {
    return document.getElementById(this.iframe_id);
  }
  get iframe_document() {
    return this.iframe.contentWindow.document
  }

  async iframe_send_button(button_name, doc) {
    let do_onclick = () => {
      function colSetSubmit(name, doc) {
        if ("ACT_backViewInfoList" == name || "ACT_cancel" == name) {
          doc.getElementById("ope_kbn_mb_impnotice_pop").value = "1";
        } else if ("ACT_deleteInfoMessage" == name) {
          doc.getElementById("ope_kbn_mb_impnotice_pop").value = "4";
        } else if ("ACT_estimate" == name) {
          doc.getElementById("ope_kbn_mb_impnotice_pop").value = "5";
        }
      }
    };
    let copy_submit_to_hidden = (button_name) => {
      let selector = `input[type='submit'][name^=${button_name}]`
      let form = doc.querySelector(selector).form
      let value = doc.querySelector(selector).value
      let input = document.createElement('input')
      input.value = value
      input.name = button_name
      input.type = 'hidden'
      form.appendChild(input)
    }
    do_onclick(button_name)
    copy_submit_to_hidden(button_name)
    doc.querySelector(`[type='submit'][name^=${button_name}]`).form.submit()
  }
  notice_exists() {
    let doc = this.iframe_document
    let link = doc.querySelector('td[class="mtext"][colspan="3"] a')
    return link != null
  }
  async click_notice() {
    let doc = this.iframe_document
    let link = doc.querySelector('td[class="mtext"][colspan="3"] a')
    link.click()
    return new Promise((resolve, reject) => {
      this.iframe.onload = function () { resolve(this.iframe_document) }
    })
  }
  async click_confirm() {
    let doc = this.iframe_document
    this.iframe_send_button('ACT_estimate', doc)
    return new Promise((resolve, reject) => {
      this.iframe.onload = function () { resolve(this.iframe_document) }
    })
  }
  async click_delete() {
    let doc = this.iframe_document
    this.iframe_send_button('ACT_deleteInfoMessage', doc)
    return new Promise((resolve, reject) => {
      this.iframe.onload = function () { resolve(this.iframe_document) }
    })
  }
  async remove_notice() {
    let doc = this.iframe_document
    doc = await this.click_notice()
    doc = await this.click_confirm()
    doc = await this.click_delete()
    return doc
  }
  async remove_all_notice() {
    while (this.notice_exists()) {
      await this.remove_notice()
    }
  }

}

起動用スクリプト

let loader = new iframeLoader()
await loader.load_notice_iframe()
let clicker = new NoticeClicker()
clicker.remove_all_notice()

これをChrome Devで貼り付けて

ひたすら、自動でクリックさせる。

iframe は便利

ブラウザ操作とか。puppeeter や selenium ドライバやら、面倒は考えないで、サクッとページ遷移を伴う自動化を作ることができる。

スクレイピングとかもはかどるし、コンソールの変数が初期化されない。

テクニックとしては次の箇所の

  constructor() {
    this.iframe_id = 'my_worker_iframe';
  }
  get iframe(){
    return document.getElementById(this.iframe_id);
  }
  get iframe_document(){
    return this.iframe.contentWindow.document
  }

iframe 内部のDOMドキュメントを取り出す部分。これを覚えておく。

iframe = document.getElementById(this.iframe_id)
iframe.contentWindow.document

iframeを動的に作る部分、ここが大事。

sandboxでフォームは使えるようにする。

が、フレーム検出のJSは動かしてやんない(allow-scriptsを未指定)

iframe = document.createElement('iframe')
iframe.setAttribute('sandbox', 'allow-forms allow-same-origin allow-top-navigation')
iframe.setAttribute('id', 'my_worker_iframe');
iframe.setAttribute('width', '100%')
iframe.setAttribute('height', '400px')
let ele = document.querySelector(selector)
let first = ele.firstElementChild
ele.insertBefore(iframe, first)

最後に、iframeのロードをawait / promiseで待ち受ける箇所

同じコードが大量にあるのは、resolveがスコープで全部違うため。

  async load_iframe(href) {
    this.iframe.src = href
    return new Promise((resolve, reject) => {
      this.iframe.onload = function () { resolve(this.iframe.contentWindow.document) }
    })
  }

この繰り返しで、iframeに閉じ込めたら、フォーム送信のJavascriptが簡単にかけて、スクレイピングするより手軽に使える

ブックマークレットで呼び出したり、コンソールを叩くだけで、自動化しちゃえる。iframe神がかってた。

ただし、ちゃんとContent-Security-Policyが設定されるサイトでは使えないので注意。また、異なるドメインのiframeは操作できません。*3

今回のサイトはCSP非対応だったと、ドメインが同じだったのでJS コンソールからズルができた。

もうズルはしません。

今回は数年分を溜め込んだのでチート行為をしましたが、今後はこまめにチェックして削除するので許してください。さすがに180件をポチポチは嫌だし無理だよ。

*1:謎のセキュリティ仕様でframe化を拒否するJSが全てのサイトに埋め込まれている。

*2:いつまでも古い技術のまま更新しないって、セキュリティ監査では何してるんですかね

*3: 以前はX-Frame-Options、今はframe-ancestoers