Harden AI database generation safety
This commit is contained in:
@@ -5,6 +5,7 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import com.ruoyi.common.exception.ServiceException;
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
@@ -21,7 +22,7 @@ public class AiDatabaseSchemaValidator
|
|||||||
private static final Pattern INT = Pattern.compile("^int(\\(11\\))?$");
|
private static final Pattern INT = Pattern.compile("^int(\\(11\\))?$");
|
||||||
private static final Pattern VARCHAR = Pattern.compile("^varchar\\(([1-9][0-9]{0,2}|1000)\\)$");
|
private static final Pattern VARCHAR = Pattern.compile("^varchar\\(([1-9][0-9]{0,2}|1000)\\)$");
|
||||||
private static final Pattern CHAR = Pattern.compile("^char\\(([1-9][0-9]?)\\)$");
|
private static final Pattern CHAR = Pattern.compile("^char\\(([1-9][0-9]?)\\)$");
|
||||||
private static final Pattern DECIMAL = Pattern.compile("^decimal\\(([1-9][0-9]?),([1-9][0-9]?)\\)$");
|
private static final Pattern DECIMAL = Pattern.compile("^decimal\\(([1-9][0-9]?),([0-9][0-9]?)\\)$");
|
||||||
private static final Set<String> EXACT_TYPES = new HashSet<String>(Arrays.asList("datetime", "date", "text", "longtext"));
|
private static final Set<String> EXACT_TYPES = new HashSet<String>(Arrays.asList("datetime", "date", "text", "longtext"));
|
||||||
private static final List<String> DANGEROUS_SQL = Arrays.asList(
|
private static final List<String> DANGEROUS_SQL = Arrays.asList(
|
||||||
"drop", "truncate", "alter user", "grant", "revoke", "load_file", "into outfile");
|
"drop", "truncate", "alter user", "grant", "revoke", "load_file", "into outfile");
|
||||||
@@ -39,7 +40,19 @@ public class AiDatabaseSchemaValidator
|
|||||||
}
|
}
|
||||||
String type = columnType.trim().toLowerCase(Locale.ENGLISH);
|
String type = columnType.trim().toLowerCase(Locale.ENGLISH);
|
||||||
return EXACT_TYPES.contains(type) || BIGINT.matcher(type).matches() || INT.matcher(type).matches()
|
return EXACT_TYPES.contains(type) || BIGINT.matcher(type).matches() || INT.matcher(type).matches()
|
||||||
|| VARCHAR.matcher(type).matches() || CHAR.matcher(type).matches() || DECIMAL.matcher(type).matches();
|
|| VARCHAR.matcher(type).matches() || CHAR.matcher(type).matches() || isAllowedDecimal(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAllowedDecimal(String type)
|
||||||
|
{
|
||||||
|
Matcher matcher = DECIMAL.matcher(type);
|
||||||
|
if (!matcher.matches())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int precision = Integer.parseInt(matcher.group(1));
|
||||||
|
int scale = Integer.parseInt(matcher.group(2));
|
||||||
|
return scale <= precision;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSafeSql(String sql)
|
public boolean isSafeSql(String sql)
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import com.ruoyi.generator.mapper.front.FrontProjectTableMapper;
|
|||||||
@Service
|
@Service
|
||||||
public class AiGenerateServiceImpl implements IAiGenerateService
|
public class AiGenerateServiceImpl implements IAiGenerateService
|
||||||
{
|
{
|
||||||
|
private static final int MAX_ERROR_MESSAGE_LENGTH = 1000;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IDeepSeekClient deepSeekClient;
|
private IDeepSeekClient deepSeekClient;
|
||||||
@Autowired
|
@Autowired
|
||||||
@@ -68,7 +70,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
catch (RuntimeException e)
|
catch (RuntimeException e)
|
||||||
{
|
{
|
||||||
generation.setSuccess("0");
|
generation.setSuccess("0");
|
||||||
generation.setErrorMessage(e.getMessage());
|
generation.setErrorMessage(sanitizeErrorMessage(e.getMessage()));
|
||||||
generation.setElapsedMs(System.currentTimeMillis() - start);
|
generation.setElapsedMs(System.currentTimeMillis() - start);
|
||||||
generationRecordService.insert(generation);
|
generationRecordService.insert(generation);
|
||||||
throw e;
|
throw e;
|
||||||
@@ -208,6 +210,20 @@ public class AiGenerateServiceImpl implements IAiGenerateService
|
|||||||
return generation;
|
return generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sanitizeErrorMessage(String message)
|
||||||
|
{
|
||||||
|
if (message == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String sanitized = message.replaceAll("[\\p{Cntrl}]", " ");
|
||||||
|
if (sanitized.length() <= MAX_ERROR_MESSAGE_LENGTH)
|
||||||
|
{
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
|
return sanitized.substring(0, MAX_ERROR_MESSAGE_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
private FrontProjectTable toFrontProjectTable(Long projectId, Long userId, DatabaseTableDesign source)
|
||||||
{
|
{
|
||||||
FrontProjectTable table = new FrontProjectTable();
|
FrontProjectTable table = new FrontProjectTable();
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ public class MysqlDdlBuilder
|
|||||||
|
|
||||||
private String escapeComment(String comment)
|
private String escapeComment(String comment)
|
||||||
{
|
{
|
||||||
return StringUtils.defaultString(comment).replace("'", "''");
|
return StringUtils.defaultString(comment)
|
||||||
|
.replaceAll("[\\p{Cntrl}]", " ")
|
||||||
|
.replace("\\", "\\\\")
|
||||||
|
.replace("'", "''");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ public class AiDatabaseSchemaValidatorTest
|
|||||||
assertFalse(validator.isValidName("sys user"));
|
assertFalse(validator.isValidName("sys user"));
|
||||||
assertTrue(validator.isAllowedColumnType("varchar(100)"));
|
assertTrue(validator.isAllowedColumnType("varchar(100)"));
|
||||||
assertTrue(validator.isAllowedColumnType("decimal(10,2)"));
|
assertTrue(validator.isAllowedColumnType("decimal(10,2)"));
|
||||||
|
assertTrue(validator.isAllowedColumnType("decimal(10,0)"));
|
||||||
|
assertFalse(validator.isAllowedColumnType("decimal(5,9)"));
|
||||||
assertFalse(validator.isAllowedColumnType("json"));
|
assertFalse(validator.isAllowedColumnType("json"));
|
||||||
assertFalse(validator.isSafeSql("drop table sys_user"));
|
assertFalse(validator.isSafeSql("drop table sys_user"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.generator.service.front;
|
package com.ruoyi.generator.service.front;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -90,7 +91,7 @@ public class AiGenerateServiceImplTest
|
|||||||
@Test
|
@Test
|
||||||
public void generateDatabasePersistsFailureGenerationRecordBeforePropagatingClientFailure()
|
public void generateDatabasePersistsFailureGenerationRecordBeforePropagatingClientFailure()
|
||||||
{
|
{
|
||||||
RuntimeException failure = new RuntimeException("DeepSeek unavailable");
|
RuntimeException failure = new RuntimeException("DeepSeek unavailable\n" + repeated("x", 1200));
|
||||||
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
|
||||||
when(deepSeekClient.chat(anyString())).thenThrow(failure);
|
when(deepSeekClient.chat(anyString())).thenThrow(failure);
|
||||||
|
|
||||||
@@ -107,7 +108,8 @@ public class AiGenerateServiceImplTest
|
|||||||
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
|
ArgumentCaptor<FrontProjectGeneration> generationCaptor = ArgumentCaptor.forClass(FrontProjectGeneration.class);
|
||||||
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
|
verify(frontProjectGenerationMapper).insertFrontProjectGeneration(generationCaptor.capture());
|
||||||
assertEquals("0", generationCaptor.getValue().getSuccess());
|
assertEquals("0", generationCaptor.getValue().getSuccess());
|
||||||
assertEquals("DeepSeek unavailable", generationCaptor.getValue().getErrorMessage());
|
assertTrue(generationCaptor.getValue().getErrorMessage().length() <= 1000);
|
||||||
|
assertFalse(generationCaptor.getValue().getErrorMessage().contains("\n"));
|
||||||
verify(frontProjectColumnMapper, never()).deleteColumnsByProjectId(10L);
|
verify(frontProjectColumnMapper, never()).deleteColumnsByProjectId(10L);
|
||||||
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
verify(frontProjectTableMapper, never()).deleteTablesByProjectId(10L);
|
||||||
}
|
}
|
||||||
@@ -155,6 +157,16 @@ public class AiGenerateServiceImplTest
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String repeated(String value, int count)
|
||||||
|
{
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
builder.append(value);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
private String mockResponse()
|
private String mockResponse()
|
||||||
{
|
{
|
||||||
return "{\n"
|
return "{\n"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ruoyi.generator.service.front;
|
package com.ruoyi.generator.service.front;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -32,6 +33,22 @@ public class MysqlDdlBuilderTest
|
|||||||
assertTrue(sql.endsWith("COMMENT='车辆''信息表';"));
|
assertTrue(sql.endsWith("COMMENT='车辆''信息表';"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void escapesBackslashQuoteSequencesInsideComments()
|
||||||
|
{
|
||||||
|
DatabaseTableDesign table = new DatabaseTableDesign();
|
||||||
|
table.setTableName("car_info");
|
||||||
|
table.setTableComment("safe \\'; update front_project set project_name='owned'");
|
||||||
|
table.setColumns(Arrays.asList(
|
||||||
|
column("car_id", "bigint(20)", "1", "1", "1", "safe \\'; update front_project set project_name='owned'")));
|
||||||
|
|
||||||
|
String sql = builder.build(table);
|
||||||
|
|
||||||
|
assertFalse(sql.contains("\\'; update"));
|
||||||
|
assertTrue(sql.contains("COMMENT 'safe \\\\''; update front_project set project_name=''owned'''"));
|
||||||
|
assertTrue(sql.endsWith("COMMENT='safe \\\\''; update front_project set project_name=''owned''';"));
|
||||||
|
}
|
||||||
|
|
||||||
private DatabaseColumnDesign column(String name, String type, String isPk, String isIncrement,
|
private DatabaseColumnDesign column(String name, String type, String isPk, String isIncrement,
|
||||||
String isRequired, String comment)
|
String isRequired, String comment)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user