Haruskah Anda melindungi terhadap nilai yang tidak terduga dari API eksternal?


51

Katakanlah Anda mengode fungsi yang mengambil input dari API eksternal MyAPI.

API eksternal itu MyAPImemiliki kontrak yang menyatakan akan mengembalikan a stringatau a number.

Apakah disarankan untuk menjaga terhadap hal-hal seperti null, undefined, boolean, dll meskipun itu bukan bagian dari API dari MyAPI? Secara khusus, karena Anda tidak memiliki kendali atas API tersebut, Anda tidak dapat membuat jaminan melalui sesuatu seperti analisis tipe statis sehingga lebih baik aman daripada menyesal?

Saya sedang berpikir sehubungan dengan Prinsip Robustness .


16
Apa dampak dari tidak menangani nilai-nilai yang tidak terduga itu jika dikembalikan? Bisakah Anda hidup dengan dampak ini? Apakah sepadan dengan kompleksitas untuk menangani nilai-nilai yang tidak terduga itu untuk mencegah keharusan berurusan dengan dampaknya?
Vincent Savard

55
Jika Anda mengharapkannya, maka menurut definisi mereka tidak terduga.
Mason Wheeler

28
Ingat API tidak berkewajiban untuk hanya memberikan Anda kembali JSON yang valid (Saya berasumsi ini adalah JSON). Anda juga bisa mendapatkan balasan seperti<!doctype html><html><head><title>504 Gateway Timeout</title></head><body>The server was unable to process your request. Make sure you have typed the address correctly. If the problem persists, please try again later.</body></html>
user253751

5
Apa yang dimaksud dengan "API eksternal"? Apakah masih di bawah kendali Anda?
Deduplicator

11
"Seorang programmer yang baik adalah seseorang yang berpandangan dua arah sebelum menyeberang jalan satu arah."
jeroen_de_schutter

Jawaban:


103

Anda tidak boleh mempercayai input ke perangkat lunak Anda, apa pun sumbernya. Tidak hanya memvalidasi jenis-jenis itu penting, tetapi juga rentang input dan logika bisnis juga. Per komentar, ini dijelaskan dengan baik oleh OWASP

Jika tidak melakukannya, paling tidak akan meninggalkan Anda dengan data sampah yang harus Anda bersihkan nanti, tetapi paling buruk Anda akan meninggalkan peluang untuk eksploitasi berbahaya jika layanan hulu itu dikompromikan dengan cara tertentu (qv Target hack). Berbagai masalah di antaranya termasuk membuat aplikasi Anda dalam kondisi yang tidak dapat dipulihkan.


Dari komentar saya dapat melihat bahwa mungkin jawaban saya bisa menggunakan sedikit ekspansi.

Dengan "jangan pernah mempercayai input", saya hanya bermaksud bahwa Anda tidak dapat berasumsi bahwa Anda akan selalu menerima informasi yang valid dan dapat dipercaya dari sistem hulu atau hilir, dan oleh karena itu Anda harus selalu membersihkan input tersebut dengan kemampuan terbaik Anda, atau menolak saya t.

Satu argumen muncul di komentar yang akan saya bahas melalui contoh. Meskipun ya, Anda harus mempercayai OS Anda sampai taraf tertentu, itu tidak masuk akal untuk, misalnya, menolak hasil generator angka acak jika Anda memintanya untuk angka antara 1 dan 10 dan merespons dengan "bob".

Demikian pula, dalam kasus OP, Anda harus memastikan bahwa aplikasi Anda hanya menerima input yang valid dari layanan hulu. Apa yang Anda lakukan saat tidak apa-apa terserah Anda, dan sangat tergantung pada fungsi bisnis aktual yang ingin Anda capai, tetapi minimal Anda akan mencatatnya untuk debugging nanti dan memastikan aplikasi Anda tidak berjalan dalam keadaan tidak dapat dipulihkan atau tidak aman.

Meskipun Anda tidak akan pernah tahu setiap input yang mungkin diberikan seseorang / sesuatu kepada Anda, Anda tentu dapat membatasi apa yang diperbolehkan berdasarkan persyaratan bisnis dan melakukan beberapa bentuk input whitelist berdasarkan itu.


20
Apa artinya qv berdiri?
JonH

15
@JonH pada dasarnya "lihat juga" ... Peretasan Target adalah contoh yang dia referensikan en.oxforddictionaries.com/definition/qv .
andrewtweber

8
Jawaban ini adalah karena memang tidak masuk akal. Sangat tidak mungkin untuk mengantisipasi masing-masing dan setiap cara perpustakaan pihak ketiga mungkin berperilaku tidak pantas. Jika dokumentasi fungsi perpustakaan secara eksplisit memastikan bahwa hasilnya akan selalu memiliki beberapa properti, maka Anda harus dapat mengandalkannya bahwa perancang memastikan properti ini benar-benar akan berlaku. Merupakan tanggung jawab mereka untuk memiliki rangkaian uji yang memeriksa hal semacam ini, dan mengirimkan perbaikan bug jika ada situasi yang tidak ditemui. Anda memeriksa properti ini dalam kode Anda sendiri melanggar prinsip KERING.
leftaroundabout

23
@leftaroundtentang tidak, tetapi Anda harus dapat memprediksi semua hal yang valid yang dapat diterima aplikasi Anda dan menolak sisanya.
Paul

10
@leftaroundabout Ini bukan tentang tidak mempercayai segalanya, ini tentang ketidakpercayaan sumber eksternal yang tidak dipercaya. Ini semua tentang pemodelan ancaman. Jika Anda belum melakukan hal itu, perangkat lunak Anda tidak aman (bagaimana bisa, jika Anda bahkan tidak pernah berpikir tentang aktor dan ancaman seperti apa yang Anda inginkan untuk mengamankan aplikasi Anda?). Untuk menjalankan perangkat lunak bisnis pabrik, merupakan standar yang masuk akal untuk mengasumsikan bahwa penelepon dapat berbahaya, sementara jarang menganggap bahwa OS Anda adalah ancaman.
Voo

33

Ya tentu saja Tetapi apa yang membuat Anda berpikir jawabannya bisa berbeda?

Anda tentu tidak ingin membiarkan program Anda berperilaku tidak terduga jika API tidak mengembalikan apa yang dikatakan kontrak, bukan? Jadi setidaknya Anda harus berurusan dengan perilaku seperti entah bagaimana . Bentuk minimal penanganan kesalahan selalu sepadan dengan usaha (sangat minimal!), Dan sama sekali tidak ada alasan untuk tidak menerapkan sesuatu seperti ini.

Namun, berapa banyak usaha yang harus Anda investasikan untuk menangani kasus seperti ini sangat tergantung pada kasus dan hanya dapat dijawab dalam konteks sistem Anda. Seringkali, entri log pendek dan membiarkan aplikasi berakhir dengan anggun sudah cukup. Terkadang, Anda akan lebih baik untuk menerapkan beberapa penanganan pengecualian terperinci, berurusan dengan berbagai bentuk nilai pengembalian "salah", dan mungkin harus menerapkan beberapa strategi mundur.

Tapi itu membuat perbedaan besar jika Anda menulis hanya beberapa aplikasi format spreadsheet in-house, untuk digunakan oleh kurang dari 10 orang dan di mana dampak finansial dari crash aplikasi cukup rendah, atau jika Anda membuat mengemudi mobil otonom baru sistem, di mana aplikasi crash mungkin menelan biaya.

Jadi tidak ada jalan pintas untuk merefleksikan apa yang Anda lakukan , menggunakan akal sehat Anda selalu wajib.


Apa yang harus dilakukan adalah keputusan lain. Anda mungkin mengalami kegagalan atas solusi. Apa pun yang tidak sinkron dapat dicoba lagi sebelum membuat log pengecualian (atau huruf mati). Peringatan aktif ke vendor atau penyedia mungkin menjadi pilihan jika masalah berlanjut.
mckenzm

@ mckenzm: fakta OP mengajukan pertanyaan di mana jawaban literal jelas hanya "ya" adalah IMHO tanda mereka mungkin tidak hanya tertarik pada jawaban literal. Tampaknya mereka bertanya "apakah perlu untuk menjaga terhadap berbagai bentuk nilai tak terduga dari API dan menghadapinya secara berbeda" ?
Doc Brown

1
hmm, pendekatan omong kosong / karper / mati. Apakah ini merupakan kesalahan kami karena menyampaikan permintaan yang buruk (tetapi sah)? apakah responsnya mungkin, tetapi tidak dapat digunakan untuk kita secara khusus? atau apakah responsnya rusak? Skenario yang berbeda, Sekarang itu terdengar seperti pekerjaan rumah.
mckenzm

21

Prinsip Robustness - khususnya, "menjadi liberal dalam apa yang Anda terima" setengahnya - adalah ide yang sangat buruk dalam perangkat lunak. Awalnya dikembangkan dalam konteks perangkat keras, di mana kendala fisik membuat toleransi rekayasa sangat penting, tetapi dalam perangkat lunak, ketika seseorang mengirimi Anda masukan yang salah atau tidak benar, Anda memiliki dua pilihan. Anda dapat menolaknya, (lebih baik dengan penjelasan tentang apa yang salah), atau Anda dapat mencoba mencari tahu apa artinya itu.

EDIT: Ternyata saya salah dalam pernyataan di atas. Prinsip Robustness tidak berasal dari dunia perangkat keras, tetapi dari arsitektur Internet, khususnya RFC 1958 . Ini menyatakan:

3.9 Ketat saat mengirim dan toleran saat menerima. Implementasi harus mengikuti spesifikasi dengan tepat saat mengirim ke jaringan, dan mentolerir input yang salah dari jaringan. Jika ragu, buang input yang salah secara diam-diam, tanpa mengembalikan pesan kesalahan kecuali ini diminta oleh spesifikasi.

Ini, jelasnya, hanya salah dari awal hingga akhir. Sulit untuk memahami gagasan penanganan kesalahan yang lebih salah daripada "membuang input yang salah secara diam-diam tanpa mengembalikan pesan kesalahan," karena alasan yang diberikan dalam posting ini.

Lihat juga makalah IETF Konsekuensi yang Berbahaya dari Prinsip Robustness untuk penjelasan lebih lanjut tentang hal ini.

Tidak pernah, tidak pernah, tidak pernah memilih opsi kedua kecuali Anda memiliki sumber daya yang setara dengan tim Pencarian Google untuk mengerjakan proyek Anda, karena itulah yang diperlukan untuk membuat program komputer yang melakukan apa pun yang dekat dengan pekerjaan yang layak di domain masalah tertentu. (Dan bahkan kemudian, saran Google terasa seperti mereka keluar langsung dari bidang kiri sekitar separuh waktu.) Jika Anda mencoba untuk melakukannya, apa yang Anda akan berakhir dengan adalah sakit kepala besar di mana program Anda akan sering mencoba untuk menafsirkan input buruk sebagai X, padahal yang dimaksud pengirim adalah Y.

Ini buruk karena dua alasan. Yang jelas adalah karena Anda memiliki data buruk di sistem Anda. Yang kurang jelas adalah bahwa dalam banyak kasus, baik Anda maupun pengirim tidak akan menyadari bahwa ada yang salah sampai jauh di kemudian hari ketika sesuatu meledak di wajah Anda, dan kemudian tiba-tiba Anda memiliki kekacauan besar yang mahal untuk diperbaiki dan tidak tahu apa yang salah karena efek yang terlihat sangat jauh dari akar penyebabnya.

Inilah sebabnya mengapa prinsip Fail Fast ada; simpan semua orang yang terlibat sakit kepala dengan menerapkannya ke API Anda.


7
Sementara saya setuju dengan prinsip apa yang Anda katakan, saya pikir Anda salah WRT maksud dari Prinsip Robustness. Saya tidak pernah melihatnya dimaksudkan untuk berarti, "menerima data yang buruk", hanya, "jangan terlalu fiddly tentang data yang baik". Misalnya, jika inputnya berupa file CSV, Prinsip Robustness tidak akan menjadi argumen yang valid untuk mencoba menguraikan tanggal dalam format yang tidak terduga, tetapi akan mendukung argumen yang menyimpulkan urutan kolom dari baris header adalah ide yang bagus. .
Morgen

9
@Morgen: Prinsip ketahanan digunakan untuk menyarankan bahwa browser harus menerima HTML yang agak ceroboh, dan menyebabkan situs web yang dikerahkan menjadi lebih ceroboh dari sebelumnya jika browser telah menuntut HTML yang tepat. Namun, sebagian besar masalah di sana adalah penggunaan format umum untuk konten yang dibuat oleh manusia dan yang dihasilkan mesin, yang bertentangan dengan penggunaan format yang dapat diedit-manusia dan format yang dapat diurai-mesin bersama dengan utilitas untuk mengkonversi di antara keduanya.
supercat

9
@supercat: namun - atau hanya karena itu - HTML dan WWW sangat sukses ;-)
Doc Brown

11
@DocBrown: Banyak hal yang benar-benar mengerikan telah menjadi standar hanya karena mereka adalah pendekatan pertama yang tersedia ketika seseorang dengan banyak pengaruh perlu mengadopsi sesuatu yang memenuhi kriteria minimal tertentu, dan pada saat mereka mendapatkan daya tarik itu adalah terlambat untuk memilih sesuatu yang lebih baik.
supercat

5
@supercat Persis. JavaScript langsung terlintas dalam pikiran, misalnya ...
Mason Wheeler

13

Secara umum, kode harus dibangun untuk menjunjung tinggi setidaknya kendala-kendala berikut bilamana praktis:

  1. Ketika diberi input yang benar, hasilkan output yang benar.

  2. Ketika diberikan input yang valid (yang mungkin atau mungkin tidak benar), hasilkan output yang valid (juga).

  3. Ketika diberi input yang tidak valid, proses tanpa efek samping apa pun di luar yang disebabkan oleh input normal atau yang didefinisikan sebagai menandakan kesalahan.

Dalam banyak situasi, program pada dasarnya akan melewati berbagai potongan data tanpa terlalu peduli apakah mereka valid. Jika potongan seperti itu mengandung data yang tidak valid, maka output program kemungkinan akan berisi data yang tidak valid sebagai konsekuensinya. Kecuali jika suatu program secara khusus dirancang untuk memvalidasi semua data, dan menjamin bahwa itu tidak akan menghasilkan output yang tidak valid bahkan ketika diberi input yang tidak valid , program yang memproses outputnya harus memungkinkan untuk kemungkinan data yang tidak valid di dalamnya.

Meskipun memvalidasi data sejak dini sering diinginkan, itu tidak selalu praktis. Antara lain, jika validitas satu bongkahan data tergantung pada isi bongkahan lain, dan jika mayoritas data dimasukkan ke dalam beberapa langkah langkah akan disaring sepanjang jalan, membatasi validasi ke data yang membuatnya melalui semua tahap dapat menghasilkan kinerja yang jauh lebih baik daripada mencoba memvalidasi semuanya.

Selanjutnya, bahkan jika program hanya diharapkan untuk diberikan data yang pra-divalidasi, sering baik untuk memiliki itu menegakkan kendala di atas pula setiap kali praktis. Mengulangi validasi penuh pada setiap langkah pemrosesan sering kali akan menguras kinerja utama, tetapi jumlah terbatas validasi yang diperlukan untuk menegakkan kendala di atas mungkin jauh lebih murah.


Kemudian semuanya berakhir untuk memutuskan apakah hasil dari panggilan API adalah "input".
mastov

@mastov: Jawaban untuk banyak pertanyaan akan tergantung pada bagaimana seseorang mendefinisikan "input" dan "perilaku yang dapat diamati" / "output". Jika tujuan program adalah untuk memproses angka yang disimpan dalam file, inputnya dapat didefinisikan sebagai urutan angka (dalam hal ini hal-hal yang bukan angka tidak dapat dimasukkan), atau sebagai file (dalam hal apa pun itu bisa muncul dalam file akan menjadi input yang mungkin).
supercat

3

Mari kita bandingkan dua skenario dan coba sampai pada kesimpulan.

Skenario 1 Aplikasi kami mengasumsikan API eksternal akan berperilaku sesuai perjanjian.

Skenario 2 Aplikasi kami menganggap API eksternal dapat berperilaku tidak benar, maka tambahkan tindakan pencegahan.

Secara umum, ada peluang bagi API atau perangkat lunak apa pun untuk melanggar perjanjian; mungkin karena bug atau kondisi yang tidak terduga. Bahkan API mungkin mengalami masalah dalam sistem internal yang menghasilkan hasil yang tidak terduga.

Jika program kami ditulis dengan asumsi API eksternal akan mematuhi perjanjian dan menghindari menambahkan tindakan pencegahan; siapa yang akan menjadi pihak yang menghadapi masalah? Itu akan menjadi kita, orang-orang yang telah menulis kode integrasi.

Misalnya, nilai nol yang telah Anda pilih. Katakanlah, sesuai perjanjian API, respons harus memiliki nilai bukan-nol; tetapi jika tiba-tiba dilanggar program kami akan menghasilkan NPE.

Jadi, saya percaya akan lebih baik untuk memastikan aplikasi Anda memiliki beberapa kode tambahan untuk mengatasi skenario yang tidak terduga.


1

Anda harus selalu memvalidasi data yang masuk - yang dimasukkan pengguna atau lainnya - sehingga Anda harus memiliki proses untuk menangani ketika data yang diambil dari API eksternal ini tidak valid.

Secara umum, setiap lapisan di mana sistem ekstra-orgranizasional bertemu harus memerlukan otentikasi, otorisasi (jika tidak didefinisikan hanya dengan otentikasi), dan validasi.


1

Secara umum, ya, Anda harus selalu waspada terhadap input yang cacat, tetapi tergantung pada jenis API, "penjaga" artinya berbeda.

Untuk API eksternal ke server, Anda tidak ingin secara tidak sengaja membuat perintah yang membuat crash atau membahayakan kondisi server, jadi Anda harus berjaga-jaga terhadap hal itu.

Untuk API seperti misalnya kelas wadah (daftar, vektor, dll), melempar pengecualian adalah hasil yang sangat baik, mengkompromikan keadaan instance kelas mungkin dapat diterima sampai batas tertentu (misalnya wadah yang diurutkan disediakan dengan operator perbandingan yang rusak tidak akan disortir), bahkan menabrak aplikasi mungkin dapat diterima, tetapi kompromi keadaan aplikasi - misalnya menulis ke lokasi memori acak yang tidak terkait dengan instance kelas - kemungkinan besar tidak.


0

Untuk memberikan pendapat yang sedikit berbeda: Saya pikir dapat diterima untuk hanya bekerja dengan data yang Anda berikan, bahkan jika itu melanggar kontraknya. Ini tergantung pada penggunaan: Ini adalah sesuatu yang HARUS menjadi string untuk Anda, atau itu sesuatu yang Anda hanya tampilkan / tidak gunakan dll. Dalam kasus terakhir, cukup terima saja. Saya memiliki API yang hanya membutuhkan 1% dari data yang dikirimkan oleh api lain. Saya tidak peduli dengan data apa yang ada di 99%, jadi saya tidak akan pernah memeriksanya.

Harus ada keseimbangan antara "mengalami kesalahan karena saya tidak cukup memeriksa input saya" dan "Saya menolak data yang valid karena saya terlalu ketat".


2
"Saya memiliki API yang hanya membutuhkan 1% dari data yang dikirimkan oleh api lain." Ini kemudian membuka pertanyaan mengapa API Anda mengharapkan data 100 kali lebih banyak dari yang sebenarnya dibutuhkan. Jika Anda perlu menyimpan data buram untuk diteruskan, Anda tidak benar-benar harus spesifik tentang apa itu dan tidak harus mendeklarasikannya dalam format tertentu, dalam hal ini penelepon tidak akan melanggar kontrak Anda .
Voo

1
@ Vo - Kecurigaan saya adalah mereka memanggil beberapa API eksternal (seperti "dapatkan rincian cuaca untuk kota X") dan kemudian memilih data yang mereka butuhkan ("suhu saat ini") dan mengabaikan data yang dikembalikan ("curah hujan") "," angin "," suhu perkiraan "," angin dinginkan ", dll ...)
Stobor

@ChristianSauer - Saya pikir Anda tidak jauh dari apa konsensus yang lebih luas - 1% dari data yang Anda gunakan masuk akal untuk diperiksa, tetapi 99% yang tidak perlu Anda tidak perlu diperiksa. Anda hanya perlu memeriksa hal-hal yang dapat membuat kode Anda tersandung.
Stobor

0

Pandangan saya tentang ini adalah untuk selalu, selalu memeriksa setiap input ke sistem saya. Itu berarti setiap parameter yang dikembalikan dari API harus diperiksa, bahkan jika program saya tidak menggunakannya. Saya cenderung juga memeriksa setiap parameter yang saya kirim ke API untuk kebenaran. Hanya ada dua pengecualian untuk aturan ini, lihat di bawah.

Alasan pengujian adalah bahwa jika karena alasan tertentu API / input salah, program saya tidak dapat mengandalkan apa pun. Mungkin program saya ditautkan ke versi lama API yang melakukan sesuatu yang berbeda dari apa yang saya yakini? Mungkin program saya menemukan bug di program eksternal yang belum pernah terjadi sebelumnya. Atau bahkan lebih buruk, terjadi sepanjang waktu tetapi tidak ada yang peduli! Mungkin program eksternal ditipu oleh peretas untuk mengembalikan barang yang dapat merusak program atau sistem saya?

Dua pengecualian untuk menguji segala sesuatu di dunia saya adalah:

  1. Kinerja setelah pengukuran kinerja yang cermat:

    • jangan pernah mengoptimalkan sebelum Anda mengukur. Menguji semua input / data yang dikembalikan paling sering membutuhkan waktu yang sangat kecil dibandingkan dengan panggilan yang sebenarnya sehingga menghapusnya sering menghemat sedikit atau tidak sama sekali. Saya masih menyimpan kode pendeteksian kesalahan, tetapi mengomentarinya, mungkin dengan makro atau hanya berkomentar saja.
  2. Ketika Anda tidak tahu apa yang harus dilakukan dengan kesalahan

    • ada saatnya, tidak sering, ketika desain Anda tidak memungkinkan penanganan jenis kesalahan yang akan Anda temukan. Mungkin yang harus Anda lakukan adalah mencatat kesalahan, tetapi tidak ada kesalahan yang masuk dalam sistem. Hampir selalu mungkin untuk menemukan beberapa cara untuk "mengingat" kesalahan yang memungkinkan setidaknya Anda sebagai pengembang untuk memeriksanya nanti. Penghitung kesalahan adalah satu hal yang baik untuk dimiliki dalam suatu sistem, bahkan jika Anda memilih untuk tidak memiliki pencatatan.

Seberapa cermatnya memeriksa nilai input / pengembalian adalah pertanyaan penting. Sebagai contoh, jika API dikatakan mengembalikan string, saya akan memeriksa bahwa:

  • tipe data actully adalah string

  • dan panjang itu antara nilai min dan maks. Selalu periksa string untuk ukuran maksimal yang bisa ditangani oleh program saya (mengembalikan string yang terlalu besar adalah masalah keamanan klasik dalam sistem jaringan).

  • Beberapa string harus diperiksa untuk karakter atau konten "ilegal" ketika itu relevan. Jika program Anda mungkin mengirim string untuk mengatakan database nanti, sebaiknya periksa serangan database (cari injeksi SQL). Tes ini paling baik dilakukan di perbatasan sistem saya, di mana saya dapat menentukan dari mana serangan itu berasal dan saya bisa gagal lebih awal. Melakukan tes injeksi SQL penuh mungkin sulit ketika string kemudian digabungkan, sehingga tes harus dilakukan sebelum memanggil basis data, tetapi jika Anda dapat menemukan beberapa masalah lebih awal, ini bisa berguna.

Alasan untuk menguji parameter yang saya kirim ke API adalah untuk memastikan bahwa saya mendapatkan hasil yang benar kembali. Sekali lagi, melakukan tes-tes ini sebelum memanggil API mungkin tampak tidak perlu tetapi membutuhkan kinerja yang sangat sedikit dan mungkin menangkap kesalahan dalam program saya. Oleh karena itu tes paling berharga ketika mengembangkan suatu sistem (tetapi saat ini setiap sistem tampaknya dalam pengembangan berkelanjutan). Bergantung pada parameter, tes bisa lebih atau kurang menyeluruh, tetapi saya cenderung menemukan bahwa Anda sering dapat mengatur nilai min dan max yang diijinkan pada sebagian besar parameter yang bisa dibuat oleh program saya. Mungkin string harus selalu memiliki minimal 2 karakter dan panjang maksimum 2000 karakter? Nilai minimum dan maksimum harus berada di dalam apa yang dimungkinkan oleh API karena saya tahu bahwa program saya tidak akan pernah menggunakan rentang penuh dari beberapa parameter.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.