#mysql #node.js #reactjs #security
#mysql #node.js #reactjs #Безопасность
Вопрос:
Я создаю простой модуль планирования, в котором я позволю любому зайти на веб-сайт, чтобы назначить встречу через планировщик.
В модуле планировщика у меня есть следующее:
- Встречи назначаются с шагом в 30 минут, поэтому, например, если встреча назначена на 9:00, она будет выполняться с 9:00 до 9:30.
- Я сохраняю личную информацию о встрече, которая была запланирована на основе пользовательского ввода, такую как номер телефона, адрес электронной почты, полное имя
- После того, как пользователь отправляет расписание, я запускаю оператор SQL select, в котором он извлекает все данные из запланированных встреч из базы данных MYSQL в компонент планировщика, чтобы пользователи могли видеть, какие встречи уже назначены и какие слоты доступны в указанное время.
Это отлично работает и все такое, но я немного обеспокоен, когда я запускаю это в производство, я заметил, что в моих сетевых запросах я вижу все данные, которые были извлечены из БД. Может ли это быть проблемой безопасности, позволяющей любому пользователю переходить к сетевым запросам для просмотра извлеченных данных, содержащих личную информацию?
Итак, мой вопрос в том, как мне это предотвратить?
Вот мой код:
ReactJS
import React, { Component } from 'react';
import appController from '../../controllers/appController';
import Scheduleservice from '../../services/Scheduleservice';
import { withRouter } from "react-router-dom";
import { Helmet } from 'react-helmet';
import Moment from 'moment';
import AOS from 'aos'; // Animate on scroll
import {
ScheduleComponent,
ResourcesDirective,
ResourceDirective,
ViewsDirective,
ViewDirective,
Inject,
TimelineViews,
Resize,
DragAndDrop,
TimelineMonth,
Day,
Week,
WorkWeek,
Month,
Agenda
} from '@syncfusion/ej2-react-schedule';
import { DateTimePickerComponent } from '@syncfusion/ej2-react-calendars';
import { DropDownListComponent } from '@syncfusion/ej2-react-dropdowns';
import { DataManager, WebApiAdaptor, ODataV4Adaptor } from '@syncfusion/ej2-data';
//import * as dataSource from './datasource.json';
class Voipcall extends Component {
constructor(props) {
super(props);
this.dataManager = new DataManager({
url: '/api/selectSchedule',
adaptor: new WebApiAdaptor,
})
this.state = {
scheduleData: [],
employeeData: [
{
Text: 'Facility Appointment',
Id: 1,
GroupId: 1,
Color: '#9e5fff',
Designation: 'Schedule an appointment',
},
],
};
}
onActionBegin(args) {
//console.log('data sent after insertion', args);
if(args.requestType === 'eventCreate' || args.requestType === 'eventChange') {
let data = args.data instanceof Array ? args.data[0] : args.data;
//console.log('Original Data', data);
// If Subject is blank
if(data.Subject !== 'Add title') {
//console.log('subject is blank')
// Format the data Role Id is guest which is 3
let insertData = {
RoleId: undefined ? "3" : "3",
Subject: data.Subject === undefined ? null : data.Subject,
FullName: data.fullName === undefined ? null : data.fullName,
Resident: data.resident === undefined ? null : data.resident,
CommunicationInfo: data.communicationInfo === undefined ? null : data.communicationInfo,
CommunicationMethod: data.communicationMethod === undefined ? null : data.communicationMethod,
Email: data.email === undefined ? null : data.email,
Phone: data.phone === undefined ? null : data.phone,
StartTime: data.StartTime === undefined ? null : appController.converttoISO(data.StartTime),
EndTime: data.EndTime === undefined ? null : appController.converttoISO(data.EndTime),
Description: data.Description === undefined ? null : data.Description
}
console.log('data insertion', insertData)
// Insert Schedule Data into DB
const insertSchedule = Scheduleservice.insertSchedule(insertData);
// Refresh the scheduler
this.props.history.push({
pathname: '/voipCall',
});
}
}
}
onEventRendered(args) {
console.log("Start time ", Moment(args.data.startTime).format());
console.log("End time ", Moment(args.data.endTime).format());
if(args.element.innerText !== 'Not Available') {
args.element.style.backgroundColor = '#0CA6C4';
args.element.innerText = 'Appointment Booked';
}
if(args.element.innerText === 'Not Available') {
args.element.style.backgroundColor = '#ccc';
}
if(args.element.innerText === 'Appointment Booked') {
args.element.style.backgroundColor = '#09A6CA';
}
}
getEmployeeName(value) {
return value.resourceData[value.resource.textField];
}
getEmployeeImage(value) {
let resourceName = this.getEmployeeName(value);
return resourceName.toLowerCase();
}
getEmployeeDesignation(value) {
return value.resourceData.Designation;
}
resourceHeaderTemplate(props) {
return (
<div className='template-wrap'>
<div className='employee-category'>
<div
className={'employee-image ' this.getEmployeeImage(props)}
></div>
<div className='employee-name'>{this.getEmployeeName(props)}</div>
<div className='employee-designation'>
{this.getEmployeeDesignation(props)}
</div>
</div>
</div>
);
}
editorTemplate(props) {
return props !== undefined ? (
<table
className='custom-event-editor'
style={{ width: '100%', cellpadding: '5' }}
>
<tbody>
<tr>
<td className='e-textlabel'>Meeting Name</td>
<td style={{ colspan: '4' }}>
<input
id='Summary'
className='e-field e-input'
type='text'
name='Subject'
style={{ width: '100%' }}
placeholder='Event Name...'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>Full Name</td>
<td style={{ colspan: '4' }}>
<input
id='fullName'
className='e-field e-input'
type='text'
name='fullName'
style={{ width: '100%' }}
placeholder='Full Name'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>Resident Requesting to Speak to</td>
<td style={{ colspan: '4' }}>
<input
id='resident'
className='e-field e-input'
type='text'
name='resident'
style={{ width: '100%' }}
placeholder='Resident you wish to speak to...'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>Preferred Method of Communication</td>
<td style={{ colspan: '4' }}>
<DropDownListComponent
id='communicationMethod'
placeholder='Choose Method of Communication'
data-name='communicationMethod'
className='e-field'
style={{ width: '100%' }}
dataSource={['Zoom', 'Skype', 'Facetime', 'Viber']}
></DropDownListComponent>
</td>
</tr>
<tr>
<td className='e-textlabel'>Communication Information</td>
<td style={{ colspan: '4' }}>
<input
id='communicationInfo'
className='e-field e-input'
type='text'
name='communicationInfo'
style={{ width: '100%' }}
placeholder='Communication ID'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>Email Address</td>
<td style={{ colspan: '4' }}>
<input
id='email'
className='e-field e-input'
type='email'
name='email'
style={{ width: '100%' }}
placeholder='Email Address'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>Phone Number ex: 123456789</td>
<td style={{ colspan: '4' }}>
<input
id='phone'
className='e-field e-input'
type='tel'
name='phone'
style={{ width: '100%' }}
placeholder='Phone Number'
/>
</td>
</tr>
<tr>
<td className='e-textlabel'>From</td>
<td style={{ colspan: '4' }}>
<DateTimePickerComponent
id='StartTime'
format='MM/dd/yy hh:mm a'
data-name='StartTime'
value={new Date(props.startTime || props.StartTime)}
className='e-field'
disabled
></DateTimePickerComponent>
</td>
</tr>
<tr>
<td className='e-textlabel'>To</td>
<td style={{ colspan: '4' }}>
<DateTimePickerComponent
id='EndTime'
format='MM/dd/yy hh:mm a'
data-name='EndTime'
value={new Date(props.endTime || props.EndTime)}
className='e-field'
disabled
></DateTimePickerComponent>
</td>
</tr>
<tr>
<td className='e-textlabel'>Reason</td>
<td style={{ colspan: '4' }}>
<textarea
id='Description'
className='e-field e-input'
name='Description'
rows={3}
cols={50}
style={{
width: '100%',
height: '60px !important',
resize: 'vertical',
}}
></textarea>
</td>
</tr>
</tbody>
</table>
) : (
<div></div>
);
}
fetchSchedule = async () => {
const selectSchedule = await Scheduleservice.selectSchedule();
await this.setState({
scheduleData: selectSchedule
})
//console.log("Schedule Data State is ", this.state.scheduleData);
//console.log("Local Datasource is ", dataSource.scheduleData);
//console.log("state is ", this.state)
}
componentDidMount = async () => {
await this.fetchSchedule();
}
render() {
const { match, location, history } = this.props;
return (
<div className='voipContainer'>
<div className='fluid-container'>
<div className='container'>
<div className='row'>
<div className='schedule-control-section'>
<div className='col-lg-12 control-section'>
<div className='control-wrapper drag-sample-wrapper'>
<div className='schedule-container'>
<ScheduleComponent
//ref={(schedule) => (this.scheduleObj = schedule)}
//ref={(schedule) => console.log("Schedule Data is", schedule)}
cssClass='block-events'
width='100%'
height='650px'
selectedDate={new Date()}
minDate={appController.previousDay(new Date())}
showTimeIndicator={false}
currentView='Day'
resourceHeaderTemplate={this.resourceHeaderTemplate.bind(
this
)}
actionBegin={this.onActionBegin.bind(this)}
eventSettings={{
dataSource: this.dataManager,
}}
eventRendered={this.onEventRendered.bind(this)}
// group={{
// enableCompactView: false,
// resources: ['Employee'],
// }}
//readonly={true}
timeScale={{ enable: true, interval: 30, slotCount: 1 }}
editorTemplate={this.editorTemplate.bind(this)}
>
<ResourcesDirective>
<ResourceDirective
field='EmployeeId'
title='Employees'
name='Employee'
allowMultiple={true}
dataSource={this.state.employeeData}
textField='Text'
idField='Id'
colorField='Color'
></ResourceDirective>
</ResourcesDirective>
<ViewsDirective>
<ViewDirective option='Day' />
<ViewDirective option='TimelineDay' />
</ViewsDirective>
<Inject
services={[
Day, Week, WorkWeek, Month, Agenda
]}
/>
</ScheduleComponent>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
);
}
}
export default withRouter(Voipcall);
Инструкция MYSQL
var db = require('../dbconnection');
var schedule = {
selectSchedule: function (data, callback) {
db.query(
'select Id, Role_Id, Subject, Location, StartTime, EndTime, Description, Owner, Priority, Recurrence, RecurrenceType, RecurrenceTypeCount, Reminder, Categorize, CustomStyle, AllDay, RecurrenceStartDate, RecurrenceEndDate, RecurrenceRule, StartTimeZone, EndTimeZone, IsBlock, isSlotAvailable, FullName, Resident, CommunicationMethod, CommunicationInfo, Email, Phone, Reason from schedule',
callback
);
},
insertSchedule: function(data, callback) {
db.query('insert schedule set Role_Id=?, Subject=?, FullName=?, Resident=?, CommunicationMethod=?, CommunicationInfo=?, Email=?, Phone=?, Reason=?, IsBlock=?, StartTime=?, EndTime=?', [data.RoleId, data.Subject, data.FullName, data.Resident, data.CommunicationMethod, data.CommunicationInfo, data.Email, data.Phone, data.Description, "true", data.StartTime, data.EndTime], callback)
//console.log('data got sent to backend', data);
}
};
module.exports = schedule;
Скриншот сетевого запроса вместе с ответом в формате JSON:
[
{
"Id":1,
"Role_Id":null,
"Subject":"Bering Sea Gold DB Test",
"Location":"chn",
"StartTime":"2020-09-23T09:00:00",
"EndTime":"2020-09-23T10:30:00",
"Description":"",
"Owner":1,
"Priority":null,
"Recurrence":1,
"RecurrenceType":null,
"RecurrenceTypeCount":null,
"Reminder":null,
"Categorize":"1,2",
"CustomStyle":null,
"AllDay":"false",
"RecurrenceStartDate":null,
"RecurrenceEndDate":null,
"RecurrenceRule":"FREQ=DAILY;INTERVAL=2;COUNT=10",
"StartTimeZone":null,
"EndTimeZone":null,
"IsBlock":"",
"isSlotAvailable":null,
"FullName":null,
"Resident":null,
"CommunicationMethod":null,
"CommunicationInfo":null,
"Email":null,
"Phone":null,
"Reason":null
},
{
"Id":2,
"Role_Id":null,
"Subject":"Not Available",
"Location":null,
"StartTime":"2020-08-01T03:00:00.000Z",
"EndTime":"2020-08-01T09:00:00.000Z",
"Description":null,
"Owner":null,
"Priority":null,
"Recurrence":0,
"RecurrenceType":null,
"RecurrenceTypeCount":null,
"Reminder":null,
"Categorize":null,
"CustomStyle":null,
"AllDay":null,
"RecurrenceStartDate":null,
"RecurrenceEndDate":null,
"RecurrenceRule":"FREQ=DAILY;INTERVAL=1;",
"StartTimeZone":null,
"EndTimeZone":null,
"IsBlock":"true",
"isSlotAvailable":null,
"FullName":null,
"Resident":null,
"CommunicationMethod":null,
"CommunicationInfo":null,
"Email":null,
"Phone":null,
"Reason":null
},
{
"Id":22,
"Role_Id":3,
"Subject":"Test Appointment 1",
"Location":null,
"StartTime":"2020-09-24T14:30:00.000Z",
"EndTime":"2020-09-24T15:00:00.000Z",
"Description":null,
"Owner":null,
"Priority":null,
"Recurrence":0,
"RecurrenceType":null,
"RecurrenceTypeCount":null,
"Reminder":null,
"Categorize":null,
"CustomStyle":null,
"AllDay":null,
"RecurrenceStartDate":null,
"RecurrenceEndDate":null,
"RecurrenceRule":null,
"StartTimeZone":null,
"EndTimeZone":null,
"IsBlock":"true",
"isSlotAvailable":null,
"FullName":"John Doe",
"Resident":"Alex West",
"CommunicationMethod":"Facetime",
"CommunicationInfo":"test@test.com",
"Email":"test@test.com",
"Phone":"999999999",
"Reason":"This is a test Module."
},
{
"Id":23,
"Role_Id":3,
"Subject":"Test Appointment 2",
"Location":null,
"StartTime":"2020-09-24T18:30:00.000Z",
"EndTime":"2020-09-24T19:00:00.000Z",
"Description":null,
"Owner":null,
"Priority":null,
"Recurrence":0,
"RecurrenceType":null,
"RecurrenceTypeCount":null,
"Reminder":null,
"Categorize":null,
"CustomStyle":null,
"AllDay":null,
"RecurrenceStartDate":null,
"RecurrenceEndDate":null,
"RecurrenceRule":null,
"StartTimeZone":null,
"EndTimeZone":null,
"IsBlock":"true",
"isSlotAvailable":null,
"FullName":"Jason Stride",
"Resident":"Chris Thomson",
"CommunicationMethod":"Facetime",
"CommunicationInfo":"test2@test.com",
"Email":"test2@test.com",
"Phone":"999999999",
"Reason":"This is test appointment 2"
}
]
Ответ №1:
Три вещи, которые следует иметь в виду:
-
используйте https: // для доставки данных в браузеры ваших пользователей. Киберпреступникам очень сложно перехватить данные, доставленные таким образом.
-
убедитесь, что каждый пользователь получает только свои собственные данные или данные, которые он имеет право просматривать. Не полагайтесь на свой интерфейсный код, чтобы скрыть данные пользователя A от пользователя B. Это не сработает, потому что любой пользователь может просмотреть ваши запросы и ответы на вкладке Сеть в devtools. Обычно вы делаете это с помощью трудно угадываемого токена сеанса, привязанного к каждому зарегистрированному пользователю.
-
Не позволяйте вашему веб-серверу возвращать какие-либо данные, которые вы не хотите, чтобы пользователь браузера видел.
Комментарии:
1. Спасибо за совет. В рабочей среде я использую https, но я думаю, что в конечном итоге я буду делать только выбор идентификатора из моего бэкэнда и отображение его всем пользователям для просмотра, а не личную информацию, которая все равно должна отображаться в планировщике. Это нормально делать?
2. На самом деле я понял. Я просто изменил свой оператор select на своем сервере, чтобы выбрать только идентификатор, который будет отображаться в планировщике, и удалил все операторы select, содержащие личную информацию для других пользователей.
3. Да, именно так. Всегда знайте идентификатор пользователя и всегда используйте
WHERE ID = ?
для идентификатора, который вы знаете, когда вы передаете конечные точки REST для доставки данных в ваше приложение.